文章目录
单例模式
Q : 简单介绍一下单例模式。
单例模式保证一个类只有一个实现,通过私有构造器和制作getInstance()方法保证单例的实现,一般分为饿汉模式和饱汉模式,饿汉模式选择当类第一次加载时就生成为一个实例,而饱汉模式则是当调用getInstance()时,才生成单例,饿汉模式没有多线程的困扰,因为JVM会保证加载类的线程安全,饱汉模式,我们一般使用延迟加载,双重检查生成单例。
单例模式很简单。
- 私有化构造方法。
- 选择懒汉加载或者饿汉加载。
- 注意多线程情况即可。
1. 私有化构造方法
构造方法是单例的天敌,想要单例就必须维持住唯一实例的地位,也就是不可替代,那么只要堵住了构造方法这条路就可以单例啦。
PS : 好比焚书坑儒((私有构造方法),秦朝掌握绝对话语权。
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
}
2. 选择懒汉或者饿汉模式
懒汉:火不烧屁股就不去做。---->只要用不到,我这辈子都不去加载这个类。
饿汉:饿死老子了,快让老子吃肉。-----> 类加载时就初始化,浪费内存。
(1)线程不安全的懒汉式:
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
private Single single = null;
public Single getSingle() {
// 当这个方法第一次背调用时,使用。
if(single == null) {
single = new Single();
}
// 之后就是返回这个类.
return single;
}
}
(2)饿汉模式:
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
// 私有的静态变量,当Java程序运行后立刻创建此单件.
private static Single single =new Single();
public Single getSingle() {
// 始终返回single
return single;
}
}
3. 注意多线程情况
饿汉在第一瞬间吃饱了,也就是在第一次加载单例类时,JVM确保线程安全,所以饿汉单例没线程问题。
但是懒汉呢?你选择了在运行时加载,你就要承担线程不安全的代价。
简单举个例子
饿汉的线程不安全问题提出
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
private Single single = null;
public static Single getSingle() {
// 第三个线程运行到这一行
if(single == null) {
// 第二个线程运行到这一行
single = new Single();
}
// 第一个线程第一次运行到这一行
return single;
}
}
第一个线程和第二个线程最后得到的实例很可能不是一个实例。第一个线程拿到了single01,但是第一个线程新建了一个线程single02.并且之后所有线程得到的将是single02,这是很致命的问题。
解决方法?加一个同一时间只允许一个线程进入的锁synchronized即可。
(3) synchronized锁解决饿汉线程问题
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
private Single single = null;
// 第二个第三个线程在这里阻塞,直到第一个线程释放这个方法.
public static synchronized Single getSingle() {
if(single == null) {
single = new Single();
}
// 第一个线程第一次运行到这一行
return single;
}
}
明显可以看到synchronized锁住了整个方法,但是单例方法几乎所有时间都在读,如果因为初始化加了一个synchronized锁,会使原来可以并发访问的方法编程单线程访问,对效率影响极大。
接下来介绍一种对效率影响不大的方法。
(4) 双重检查解决synchronized锁方法过慢问题
双重检查可以有效解决加锁带来的效率问题。
Java中有一个volatile 关键字可以用,能有效减少因为synchronized导致的效率问题。
public class Single {
// 私有构造方法,覆盖掉默认的无参构造。
private Single() {
}
// 保证内存可见性。
private volatile Single single;
public static Single getSingle() {
if(single == null) {
// 锁住Single类,单线程阻塞进入
synchronized (Single.class) {
// 只有第一个线程可以真正进入。
if(single == null) {
single = new Single();
}
}
}
// 第一个线程第一次运行到这一行
return single;
}
}
这样即使有两个进程同时进入了第一个if语句内,其中一个也将会陷入阻塞,然后等到起run起来后无法进入第二个if。
(5) 静态内部类解决饿汉线程问题
public class Single {
// 静态内部类不会被立刻加载,只有被使用到的时候才会被加载。
private static class SingleHolder {
// 加载静态内部类时,加载单例。
private static final Single INSTANCE = new Single();
}
private Single (){}
public static final Single getInstance() {
// 此时如果SingleHolder没被加载过,那么由JVM保证线程安全的加载SingleHolder。
return SingleHolder.INSTANCE;
}
}
Tip : 我们都知道当一个类被加载时,其内部的静态变量,静态代码块,静态方法将会被加载。
但是,静态内部类不会被加载,因为在JVM眼里,就不存在内部类。
上面的代码,在我们看来就一个class文件,但是JVM认为这是两个互不相关的class文件
Single.class和SingleHolder.class文件.所以就不存在加载问题。
总结:能用饿汉最好。懒汉情况较复杂,唯一的优势是能将启动时间减少,但是在运行时不可避免地需要降低一点效率(虽然完全可以忽略).