定义:使用该模式创建的类只有一个实例,且是在该类中实例化他自己并向系统提供该实例
使用场景要求只有一个对象的场景
饿汉模式
在加载该类的时候便实例化该类,换句话说,在声明的时候就实例化了。
饿汉模式由于一开始就创建了,所以并不存在并发安全问题。
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){};
public static Singleton getInstance(){
return instance;
}
}
懒汉模式
在调用该对象的时候才实例化。实际上就是第一次调用的时候实例化,再次调用是由于该对象已实例化了,所以就直接使用;
优点就是用到的时候在实例,缺点就是第一次调用时会比较慢,而且多线程环境下会有线程安全问题
线程不安全
public class Singleton {
private static Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果有Singleton之前还没被调用过,这个时候突然来了一堆线程调用该单例,假设单例构造需要花很多时间,那么就会有很多线程判断instance==null,导致重复构造了该实例
线程安全,获取效率低
public class Singleton {
private static Singleton instance;
public static Singleton getSingleton() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这种能保证线程安全,但是在多线程环境下获取效率会很慢,因为每次获取都需要获得锁在进行判断,同一时刻线程很多的话,还会导致其他线程陷入无意义的阻塞
线程安全,效率更高 DCL写法
public class Singleton {
private static Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查,介于上面的问题, 在外层多一层判断,只有真正判断为null时,才上锁进行构造。那这个时候如果不要锁里面的判断行不行?很明显是不可以的,因为这样锁就没意义了,又回到我们前面线程不安全的情况。同时间会有很多线程判断instance为null,从而进入构造的代码。
这种写法大部分情况下已经没问题了,但还是存在DCL失效的情况,因为指令重排。
防止指令重排
public class Singleton {
private volatile static Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
instace加上了volatile关键字,只要是为了预防cpu指令重拍.这里的问题出在instance = new Singleton()
这句赋值语句;该赋值语句可以被拆成多条指令
1.分配对象的内存空间
2.初始化对象
3.设置instance指向刚分配的内存地址,当instance指向分配地址时,instance不为空
这里1肯定要在23之前,但是23顺序可以调整,假设3被调整到2之前,那么instance这个引用变量便不再是null了,其他线程在进入到这个getSingleton()方法后,判断instance !=null,返回了该变量并且直接使用,但是该变量引用实际指向的对象并未初始化,从而导致了错误; 所以这里也需要加上volatile来保证23的指向顺序;
静态内部类单例模式
public class Singleton {
private Singleton(){};
public static Singleton getSingleton(){
return SingletonHolder.sInstance;
}
public static class SingletonHolder{
private static final Singleton sInstance = new Singleton();
}
}
Singleton在初始被加载时并不是实例化sInstance,只有在第一次调用getSingleton时,才会加载SingletonHolder并初始化Singleton()。
这里有个疑问?为什么这种写法能保证线程安全呢?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的 <clinit>
方法。
<clinit>
方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>
方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>
方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>
方法。然后执行完<clinit>
方法后,其他线程唤醒,但是不会再进入<clinit>()
方法。也就是说同一个加载器下,一个类型只会初始化一次。
那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>
代码,当我们执行getInstance方法的时候,会导致SingleTonHolder类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>
代码也只会被执行一次,所以他只会有一个实例。
那么再增加一句,之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>
,也就是即使有指令重排序,因为根本没有第二个线程给你去影响,所以无所谓。
此外,如果讲得再细致一点,可以参考下面的第二个网站,简单说一下:
比如有一个类T,那么什么时候会进行类T的初始化?有以下5种情况:
T 是一个类,而且一个 T 类型的实例被创建;
T 是一个类,且 T 中声明的一个静态方法被调用;
T 中声明的一个静态字段被赋值;
T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
T 是一个顶级类(top level class,见 java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。
而我们前面的代码则对应了第四种情况,一个静态字段被使用,因此此时则会进行静态内部类的初始化。又因为java是多线程语言,作者也考虑到了可能存在“多个线程在同一时间尝试去初始化同一个类”的情况,因此java规定,对于每一个类或接口 C,都有一个唯一的初始化锁 LC 与之对应。
JVM 在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。(但是只有第一个获得锁的线程,会执行初始化,执行完之后会设置一个标志位,表示已经初始化完成,后面其他的线程再次获得锁,检查标志位,发现已经初始化完了,直接释放锁,不会再次执行初始化。)