单例设计模式

1.什么是单例设计模式

所谓的单例设计模式简单的说就是一个类只能构建一个对象的设计模式。单例有如下特点:
①、单例类只能有一个实例
②、单例类必须自己创建自己的实例
③、单例类必须提供外界获取这个实例的方法
单例设计模式有以下几种:饿汉,懒汉,双重锁,静态内部类以及枚举类。

2.饿汉设计模式(天生线程安全、调用效率高、不能延时加载)

public class Singleton {
    //私有的构造方法
    private Singleton(){}
    
    //私有静态单例对象
    private static final Singleton instance = new Singleton();
    
    public static Singleton getInstance(){
        return instance;
    }
}

3.懒汉设计模式(调用效率不高,能延时加载)

不安全的设计方式:

public class Singleton {
    //私有的构造方法
    private Singleton(){}

    //私有静态单例对象
    private static Singleton instance = null;

    //获取单例的方法
    public static Singleton getInstance(){
        if (null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

上述懒汉的写法不符合线程安全的要求,因为当Singleton第一次被初始化的时候,两个线程同时同时访问getInstance()方法,此时instance == null,两个线程同时通过了判断,执行了两个new,这样instance便被构建了两次。

举个栗子:当有三个线程同时执行的时候

   public static void main(String[] args) throws Exception {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        };
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
    }

执行结果:(当然也可能出现一样的结果,下面这个是极端的情况)
design.Singleton@893a0ba
design.Singleton@213a0ba
design.Singleton@383a0ba
分析:因为三个线程同时调用了getInstace()方法,执行了instance = null,判断都为true,各自创建了一个对象.

安全写法:
学习了多线程后可以加 synchronized 关键字在方法上实现加锁同步

public class Singleton {
    //私有的构造方法
    private Singleton(){}

    //私有静态单例对象
    private static Singleton instance = null;

    //方法同步,调用效率低
    public static synchronized Singleton getInstance(){
        if (null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

饿汉和懒汉的区别:
饿汉是主动找食物吃,懒汉是躺着等着食物吃,没有才主动去找。

4.双重锁检测

上述懒汉设计模式,虽然实现了线程安全,但是因为Synchronized将整个方法锁起来了,每个线程必须等其他的线程执行完这个方法才有机会调用此方法,如果此时方法中存在耗时操作,将会很麻烦。

举个栗子:

public static synchronized Single getInstance(){
Thread.sleep(10000);  //假如等待10秒,将会出现所有线程都要执行10秒才轮到下一个线程执行,3线程就执行30秒,多线程为了提高效率但却适得其反了这样。
    if (s == null){
        s = new Single();
    }
    return s;
}

这就引出了双重锁设计,Double CheckLock(DCL):

public class Singleton {
    //私有的构造方法
    private Singleton(){} 

    //私有静态单例对象
    private static Singleton instance = null;    
   
    //获取单例的方法
    public static Singleton getInstance(){
        if (null == instance){//双重检测机制
            synchronized (Singleton.class){//同步锁
                if (instance == null){}//双重检测
                instance = new Singleton();
            }
        }
        return instance;
    }
}

仔细看上面的代码,乍一看,没有什么问题了,但是仔细瞧,这里并非绝对的线程安全。
分析:
Java中instance = new Singleton会被编译器编译成如下JVM编译器指令
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1、3的时候,instance对象还未完成初始化,此时instance对象已经不再是null了,此时线程B抢占到CPU资源,执行if(instance == null)的结果会是false,从而直接返回了一个没有初始化完成的instance对象。

优化:

public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
          synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

Volatile:保证了变量在内存的可见性,即一个变量被volatile修饰了,那么它的改变对于各个线程都是可见的。它还阻止优化的编译器优化后续的读或写操作,从而错误地重用陈旧的值或忽略写操作
简单的来说就是保证了下述123的顺序,避免了返回未初始化完成的instance
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

5.静态内部类(线程安全,调用效率高,可以延时加载)

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton() {
    }      //私有的构造方法

    /**
     * 获取单例对象的方法
     *
     * @return Singleton
     */
    public Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

说明:
1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

6.枚举类(线程安全,调用效率高,不能延时加载,可以天然防止反射和序列化的调用)

静态内部类的方法虽然很好,但是存在着单例共同的问题:无法防止利用反射来重复构建对象。

举个栗子:

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));

打印:false
很显然,两个实例不一样

枚举:本身是个类,且是静态类,就是一个单例的,默认被final修改类,不能被继承,枚举中只有ToString没有被final修饰,枚举是自己内部实例化对象,这种其实也是一种饿汉式
优点:代码简单,防止序列化

public enum Singleton {
    INSTANCE;

    public void doSomeThing() {
        System.out.println("what do you what to do.");
    }
}
class Test{
	  public static void main(String[] args) throws Exception {
        Singleton.INSTANCE.doSomeThing();
    }
}
posted @ 2020-07-28 15:04  CherrieLin  阅读(223)  评论(0编辑  收藏  举报