1.单例模式介绍:
1)单例模式的作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
2)单例模式的优点:单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
3)单例模式有多种,评价一个单例模式的参数有,是否立刻加载还是延时加载,是否线程安全,调用效率是否高
- 立即加载和延时加载:是否是立即加载还是延时加载:立即加载是在第一次使用对象的时候对象已经加载,延时加载是在第一次使用对象的时候开始加载。
- 线程安全:在多线程情况下,是不是保证创建对象只有一个
- 调用效率是否高:如果能保证多个线程同时获取对象,调用效率就高,如果一个线程获取对象的时候,另一个线程要等待,调用效率就不高
2.常见的单例模式有五种:
常见:
- 饿汉式(线程安全,调用效率高,不能延时加载)
- 懒汉式(线程安全,调用效率不高,可以延时加载)
其他:
- 双重检测锁式(线程安全,调用效率高,可以延时加载)可以理解为懒汉式的升级版
- 静态内部类式(线程安全,调用效率高,可以延时加载)
- 枚举单例
2.饿汉式(线程安全,调用效率高,不能延时加载):
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
对饿汉式的特点说明:
立即加载:类加载的初始化阶段会去初始化类变量(静态变量),此时就开始创建对象而此时并未使用类,所以属于立即加载。
线程安全:初始化阶段也就是执行类构造器<clinit>()方法,因为虚拟机保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,所以是线程安全的(深入理解Java虚拟机226页)。
调用效率高:getInstance()没有同步,可以多个线程同时调用getInstance()方法获取同一个对象,所以调用效率高。
3.懒汉式(线程安全,调用效率不高,可以延时加载):
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
public synchronized static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
对懒汉式的特点的说明:
支持延时加载:懒汉式在类加载的时候并没有通过构造方法初始化对象,而是在第一个调用getInstance()方法的时候才创建singleton对象,所以懒汉式支持延时加载(所谓延时加载的就是用的时候再加载)。
线程安全:懒汉式的线程安全问题,是在一个线程判断if(singleton==null)时开始创建Singleton对象但没有创建完成,此时引用singleton仍然为null,此时分配给这个线程的时间片执行完了,另一个线程获取了CPU的执行权,判断if(singleton==null)后开始创建对象,导致了对象被创建了两次,懒汉式中通过synchronized关键字修饰getInstance()方法,保证了在一个线程new创建一个对象的时候,保证了别的线程不会进入getInstace()方法,所以是线程安全的。
调用效率低:懒汉式的安全问题是在创建对象的时候出现的(创建了多个对象),但是如果对象已经创建完成,此时获取对象并不存在线程安全问题,使用synchronized修饰getInstance()方法,导致在一个线程获取对象时,另一个线程获取对象要等待,这导致了调用效率低。
4.双重检测锁式(i线程安全,支持延时加载,调用效率高):
public class Singleton
{
private volatile static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
1)懒汉式因为使用synchronized修饰方法,同一个时刻只有一个线程能获取创建完的对象,导致了效率低。双重检测锁式可以解决这个问题,它通过两个if(singleton==null)判断(双重检测的名称由来),在判断singleton不为空,说明singleton对象已经被创建后,直接返回该对象不需要同步,使得多个线程可以同时获取创建好的对象。在对象还没有被创建时,此时singleton==null,通过同步语句块,保证了if(singleton==null)和singleton=new Singleton()被一个线程执行完,而不会被别的线程打断。
2)volatile关键字的作用:volatile关键字的常见的两个作用是:可见性和防止指令重排序,在双重检测模型中使用该关键字是为了防止执行重排序,实例化一个对象其实可以分为三步:(1)申请分配内存(2)调用构造方法初始化对象(3)将内存空间的地址赋值给对应的引用。由于操作系统可以进行指令重排序,导致实际的执行顺序也可能是(1)(3)(2)。在双重检测模型中,singleton= new Singleton()不是原子操作,假设出现这样一种情况,线程一执行(1)(3)(2)的顺序并且只执行完了(1)和(3),(2)还没有来得及执行,此时singleton指向了一个内存不为null,此时另一个线程获得CPU的执行权执行getInstance()方法,判断singleton!=null,直接返回singleton对象,而此时singleton还没有被创建,导致出错。
3)代码中volatile为什么没有起可见性的作用而是起到防止指令重排的作用:
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
也许第一个singleton的值没有保证可见性(读取的线程自己工作内存中的值),但是synchronized中的if(singleton==null)已经保证了singleton的可见性,所以这里的volatile还是起到防止指令重排的作用。
5.静态内部类式:
public class Singleton {
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
支持延时加载:SingletonHolder被定义为静态内部类,在Sinleton类加载的时候并不会创建singleton对象,只有getInstance()方法第一次被调用的时候,读取SingletonHolder的静态变量INSTSNCE时,立即对SinlgetonHolder类初始化(深入理解Java虚拟机210页——5中情况必须对类初试化)。
线程安全:因为初始化中的<clinit>函数是线程安全的所以,所以private static Singleton INSTANCE = new Singleton()这段代码是线程安全的。
调用效率高:获取对象的方法getInstance()并没有加同步语句,多个线程可以同时获取对象,所以效率高。
参考:
深入理解Java虚拟机
https://www.zhihu.com/question/56606703
http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
https://blog.csdn.net/cselmu9/article/details/51366946
http://blog.51cto.com/devbean/203501
https://blog.csdn.net/haoel/article/details/4028232
https://blog.csdn.net/wm5920/article/details/12562145