单例模式就是某个类最多只能创建一个对象,有很多种实现方式,下面就介绍一下这几种实现方式以及各自的优缺点。
饿汉式
饿汉式顾名思义就是在最开始就创建好该对象,即使对象没有被使用
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
//类加载时就进行初始化
private static final Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
}
这种方式线程安全,写起来比较简单,缺点就是可能会造成空间的浪费。并且如果在创建singlenton对象时需要传递参数等情况时,就无法采用这种方法了。
懒汉式
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static Singleton singleton;
//在需要获取对象时才进行初始化
public static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
这种方法不会造成空间的浪费,因为在需要获取该对象时才进行对象的创建,坏处是可能产生线程安全问题。比如进程A在第6行判断singleton为null后被切出了时间片,此时线程B判断singleton依然为null,线程B创建了对象。当线程A的时间片切回来时,因为其之前已经判断singleton为null,所以其也会创建一个singleton对象。这样就导致了在多线程情况下的创建了多个对象。
线程安全的懒汉式
因为上边实现的懒汉式存在线程安全问题,我们最简单的想法就是直接在方法上加synchronized,像下边这样:
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static Singleton singleton;
//在需要获取对象时对整个方法进行加锁
public static synchronized Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
这种方法完美的保证了线程安全问题,但代价就是需要进行线程间的同步,其实我们思考一下上述的线程不安全的懒汉式。可以发现只需要在创建对象的时候保证线程安全即可,当对象创建完成之后,其实线程安全已经没有必要了,因为if(singleton==null)这个判断不会通过,会直接返回该对象。所以我们只需要在创建对象的时候保证线程安全即可,创建完成之后就没有必要进行线程同步了。
理所应当的,我们想到了这种写法:
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static Singleton singleton;
public static Singleton getInstance(){
if(singleton==null){
//在需要创建对象时进行加锁
synchronized(Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
}
根据我们上边的分析,我们这种写法已经在代价较小的情况下实现了线程安全,只在创建对象时进行了加锁。对象创建完毕之后,就不需要保证线程安全,直接返回即可。但真的是这样吗?其实这种写法也会有线程安全的问题,在创建对象时,当两个线程都进行了if(singleton==null)的判断后,此时两个线程会先后的获取到锁,并且创建两个对象,从而导致线程安全问题。 这是有小伙伴说了,在获取到锁之后再进行一次if(singleton==null)的判断就行了,对,由此我们写出了经典的双重校验锁模式
双重校验锁
根据上边的分析,我们写出了下边的代码
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static Singleton singleton;
public static Singleton getInstance(){
if(singleton==null){ //第一次校验
synchronized(Singleton.class){
if(singleton==null){ //第二次校验
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式已经完美的解决了我们上述所分析的问题,但这种写法真的就没问题了吗?其实存在问题,主要在于singleton = new Singleton();这句在JVM层面主要做了三个动作:
- 给singleton分配内存
- 调用Singleton的构造方法
- 将singleton对象指向分配的内存空间(执行完这步之后singleton就不为null值了)
由于JVM中存在指令重排的优化,所以上述的2、3的顺序无法保证,有可能是1、2、3,也有可能是1、3、2,当是1、3、2时。在多线程情况下,比如执行顺序是1、3、2,某个线程执行完3后被切出时间片了,此时第二个线程判断if(singleton==null)已经不为null了,就会拿到一个没有被完全初始化的singleton对象,就会发生报错。这就是著名的DCL(Double Check Lock)问题。
那有没有什么方法可以防止指令重排呢?java中volatile关键字就有防止指令重排的功能,所以只需要将singleton声明为volatile就可以啦!
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static volatile Singleton singleton;
public static Singleton getInstance(){
if(singleton==null){ //第一次校验
synchronized(Singleton.class){
if(singleton==null){ //第二次校验
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样就完全的解决了我们上述的问题,实现了线程安全的懒汉式的单例模式。
静态内部类
上边我们进行了多种尝试,那有没有简单一点的,或者说利用了java特性的单例模式写法呢?这就要引出静态内部类了
public class Singleton{
//构造函数私有,防止外部初始化对象
private Singleton(){}
private static class SingletonHolder{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
这种方式也保证了线程安全,并且是懒汉式的,即在调用getInstance方法时才创建该对象。其实我第一次看到这种写法时,很疑惑,为啥线程安全呢?因为类加载过程本身就是线程安全的。为啥是懒汉呢?这个内部类不是在外部类加载的时候就加载了吗?后来去看了资料才知道,静态内部类和外部类没有关系,只是写在内部类中罢了。所以只有咋调用getInstance()方式才会加载该内部类。所以这种方法也保证了线程安全。并且充分利用了JVM的特性
枚举
public enum Singleton{
SINGLETON;
public void doSomething(){
}
}
这种方法也可以保证单例和线程安全,因为创建枚举类本身就是线程安全的,而且还能防止反序列化重新创建新的对象。但这种写法一般很少看到。