单例模式就是某个类最多只能创建一个对象,有很多种实现方式,下面就介绍一下这几种实现方式以及各自的优缺点。

饿汉式

饿汉式顾名思义就是在最开始就创建好该对象,即使对象没有被使用

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层面主要做了三个动作:

  1. 给singleton分配内存
  2. 调用Singleton的构造方法
  3. 将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(){
    
    }
}

这种方法也可以保证单例和线程安全,因为创建枚举类本身就是线程安全的,而且还能防止反序列化重新创建新的对象。但这种写法一般很少看到。