欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

单例设计模式(这一篇足够了)

程序员文章站 2022-06-30 14:39:12
单例模式真是一个老掉牙的问题了,不过我今天是要说些里面更深点的知识,闲话少说,直接来代码 1、饿汉式 相信这种写法大家都知道,一开始接触单例的时候,大家应该都是用的这种方法: 这种方式优点就是线程安全, 缺点也很明显,就是类加载的时候,就已实例化该对象了,后面有可能用不到这个实例对象,这样就会造成空 ......

 

  单例模式真是一个老掉牙的问题了,不过我今天是要说些里面更深点的知识,闲话少说,直接来代码

  1、饿汉式

   相信这种写法大家都知道,一开始接触单例的时候,大家应该都是用的这种方法:

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
}

  这种方式优点就是线程安全, 缺点也很明显,就是类加载的时候,就已实例化该对象了,后面有可能用不到这个实例对象,这样就会造成空间浪费。因此就有了懒加载方式。

  2、懒汉式

  1)懒汉式L1

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null)        //1
            instance = new Singleton2();  //2
        return instance;
    }

}

  这种懒汉式的优点和缺点也很明显,优点是按需加载,节省空间, 缺点是线程不安全。简单说就是,有可能线程A执行到“1”处时,阻塞住了,线程B抢到CPU,进来执行并实例化对象,然后线程A醒来后,继续往下执行,这样线程A和B取到的就是不同的对象。因此,又有了线程安全的版本。

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance(){
        if(instance == null)
            instance = new Singleton2();
        return instance;
    }
}

  但是加了synchronized 之后会造成线程阻塞,影响性能。于是又提出了双检锁的方式

 1 package com.hd.single;
 2 
 3 public class Singleton2 {
 4 
 5     private Singleton2(){}
 6     private static Singleton2 instance;
 7 
 8     public static Singleton2 getInstance(){
 9         if(instance == null){
10             synchronized (Singleton2.class){
11                 if(instance == null){
12                     instance = new Singleton2();
13                 }
14             }
15         }
16         return instance;
17     }
18 }

  看似双检锁的方式很完美,既解决了线程安全的问题,又兼顾了性能问题: 线程先判断instance变量是否为空,如果不为空,则直接返回。否则进入同步块去实例化对象。但事实这是一个错误的优化!

  重点就是第12行代码(instance = new Singleton2();), 它创建了一个对象。这一行代码可以分解为如下的3行代码:

memory = allocate();         //1:分配对象的内存空间
ctorInstance(memory);        //2:初始化对象
instance = memory;           //3:设置instance指向刚分配的内存地址

  上面2和3这两步之间,有可能会被重排序,2和3重排序之后的执行时序如下:

memory = allocate();         //1:分配对象的内存空间
instance = memory;           //3:设置instance指向刚分配的内存地址
                           //注意此时对象还没有被初始化
ctorInstance(memory);        //2:初始化对象
                                    

  因此如果有线程A执行到3时,此时instance变量确实不为空,然后线程B判断instance不为空后返回,那么这是时程B 取到的就是一个空的对象。显示这样是有问题的,因此为了防止出现这个问题,我们可以使用volatile变量,来禁止指令重排序(具体指令重排序知识点,可以去网上搜索相关内容,一大堆,我就不详细说了。本质就是jvm为了优化而使用的)。

  2)懒汉式 L2(基于volatile的解决方案)

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private volatile static Singleton instance;

    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

  我们除了通过volatile的方式来禁止指令重排序,还可以提供另外一种思路:允许2和3重排序,但不允许其它线程“看到”这个重排序。 前面正是因为线程B看到了重排序,发现instance变量不为空,所以才造成其取到空的对象。

  3)懒汉式L3(基于静态内部类的方案)

package com.hd.single;
public class LazySingleton2 {
    private LazySingleton2() {
    }
    static class SingletonHolder {
        private static final LazySingleton2 instance = new LazySingleton2();
    }
    public static LazySingleton2 getInstance() {
        return SingletonHolder.instance;
    }
}

  因为 在加载外部类时,其内部类不会同时被加载。只有调用 getInstance方法的时候,内部类才会去被加载,且只加载一次,不存在并发问题,因此是线程安全的。

  另外,在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。

 

  本文给出了多个版本的单例模式,供我们在项目中使用。一般用L2,L3就基本够用。