单例模式双重校验锁_单例模式创建方式竟然有7种,你知道吗?

bd64ad9fba10082b0209bd2d1aa7b74e.gif 

喜欢就关注我们吧!

单例模式说是简单,却又不简单。单说单例模式的创建方式,就有7种。

单例模式供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

应用实例:

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

1)饿汉模式(多线程安全)

饿汉模式,所谓饿汉就是在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。



1.  class Singleton {
    
2.      private static Singleton instance = new Singleton(); // 实例化对象
    
3.     //让构造函数为 private,这样该类就不会被实例化 
    
4.      private Singleton(){}
    

6.      // 创建唯一的对象
    
7.      public static Singleton getInstance(){
    
8.          return instance;
    
9.      }
    

11.      public void show(){
    
12.          System.out.println("Singleton show---");
    
13.      }
    
14.  }
    

16.  public class Main {
    
17.      public static void main(String[] args) {
    
18.          Singleton instance = Singleton.getInstance();
    
19.          instance.show();
    
20.      }
    
21.  }
    


「饿汉式」是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况,这种方式简单粗暴,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。

2)懒汉模式(线程不安全)

懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。在多线程访问的时候,很可能会造成多次实例化,就不再是单例了。



1.  class Singleton {
    
2.      private static Singleton instance;
    

4.      private Singleton(){}
    

6.      public static Singleton getInstance(){
    
7.          if(instance == null){
    
8.              instance = new Singleton();
    
9.          }
    
10.          return instance;
    
11.      }
    

13.      public void show(){
    
14.          System.out.println("Singleton show---");
    
15.      }
    
16.  }
    

18.  public class Main {
    
19.      public static void main(String[] args) {
    
20.          Singleton instance = Singleton.getInstance();
    
21.          instance.show();
    
22.      }
    
23.  }
    


3)懒汉模式(线程安全)

对创建对象的加锁,适用于多线程中,但是效率不高

因为这种方式在getInstance()方法上加了同步锁,所以在多线程情况下会造成线程阻塞,把大量的线程锁在外面,只有一个线程执行完毕才会执行下一个线程。



1.  class Singleton {
    
2.      private static Singleton instance;
    

4.      private Singleton(){}
    

6.      public static synchronized Singleton getInstance(){ // 对创建对象加速
    
7.          if(instance == null){
    
8.              instance = new Singleton();
    
9.          }
    
10.          return instance;
    
11.      }
    

13.      public void show(){
    
14.          System.out.println("Singleton show---");
    
15.      }
    
16.  }
    

18.  public class Main {
    
19.      public static void main(String[] args) {
    
20.          Singleton instance = Singleton.getInstance();
    
21.          instance.show();
    
22.      }
    
23.  }
    


4)双重校验锁 - DCL(线程安全)【推荐使用】

懒汉式(线程安全)」毫无疑问存在性能的问题 — 如果存在很多次getInstance()的调用,那性能问题就不得不考虑了

为什么双重校验锁可以保证线程安全,原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。



1.  class Singleton {
    
2.      private volatile static Singleton instance;
    

4.      private Singleton(){}
    

6.      public static synchronized Singleton getInstance(){ // 对创建对象加速
    
7.          if(instance == null){
    
8.              synchronized(Singleton.class){
    
9.                  if (instance == null){
    
10.                      instance = new Singleton();
    
11.                  }
    
12.              }
    
13.          }
    
14.          return instance;
    
15.      }
    

17.      public void show(){
    
18.          System.out.println("Singleton show---");
    
19.      }
    
20.  }
    

22.  public class Main {
    
23.      public static void main(String[] args) {
    
24.          Singleton instance = Singleton.getInstance();
    
25.          instance.show();
    
26.      }
    
27.  }
    




1.  被volatile修饰的变量的值,将不会被本地线程缓存,
    
2.  所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
    


  1. 延迟初始化。和懒汉模式一致,只有在初次调用静态方法getSingleton,才会初始化signleton实例。
  2. 性能优化。同步会造成性能下降,在同步前通过判读singleton是否初始化,减少不必要的同步开销。
  3. 线程安全。同步创建Singleton对象,同时注意到静态变量singleton使用volatile修饰。

为什么要使用volatile修饰?

虽然已经使用synchronized进行同步,但在第4步创建对象时,会有下面的伪代码:



1. memory=allocate(); //1:分配内存空间
    
2.  ctorInstance();   //2:初始化对象
    
3.  singleton=memory; //3:设置singleton指向刚排序的内存空间
    
4.  复制代码
    


当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!

「双重校验锁」:既可以达到线程安全,也可以使性能不受很大的影响,换句话说在保证线程安全的前提下,既节省空间也节省了时间,集合了「饿汉式」和两种「懒汉式」的优点,取其精华,去其槽粕。

对于volatile关键字,还是存在很多争议的。由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

5)静态内部类(线程安全)【推荐使用】

在很多情况下JVM已经为我们提供了同步控制,比如:

  • static {...}区块中初始化的数据
  • 访问final字段时

在JVM进行类加载的时候他会保证数据是同步的,我们可以这样实现:**采用内部类,在这个内部类里面去创建对象实例。**这样的话,只要应用中不使用内部类 JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现「懒汉式」的延迟加载和线程安全。



1.  class Singleton {
    

3.      private Singleton(){}
    

5.      public static Singleton getInstance(){ 
    
6.          return Inner.instance;
    
7.      }
    

9.      // 内部静态类
    
10.      private static class Inner{
    
11.          private final static Singleton instance = new Singleton();
    
12.      }
    

14.      public void show(){
    
15.          System.out.println("Singleton show---");
    
16.      }
    
17.  }
    

19.  public class Main {
    
20.      public static void main(String[] args) {
    
21.          Singleton instance = Singleton.getInstance();
    
22.          instance.show();
    
23.      }
    
24.  }
    


内部类的实现方法有以下优点:

  1. 实现代码简洁。和双重检查单例对比,静态内部类单例实现代码真的是太简洁,又清晰明了。
  2. 延迟初始化。调用getSingleton才初始化Singleton对象。
  3. 线程安全。JVM在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。

6)使用枚举

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。



1.  enum Singleton {
    

3.      INSTANCE;
    
4.      public void show(){
    
5.          System.out.println("Singleton show---");
    
6.      }
    
7.  }
    

9.  public class Main {
    
10.      public static void main(String[] args) {
    
11.          Singleton instance = Singleton.INSTANCE;
    
12.          instance.show();
    
13.      }
    
14.  }
    


7)使用容器

在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。



1.  public class SingletonManager { 
    
2.    private static Map objMap = new HashMap();3.    private Singleton() { 4.    }5.    public static void registerService(String key, Objectinstance) { 6.      if (!objMap.containsKey(key) ) { 7.        objMap.put(key, instance) ;8.      }9.    }10.    public static Object  getService(String key) { 11.      return objMap.get(key) ;12.    }13.  }

往期推荐  

计算机TCP/IP面试10连问,你能顶住几道?

看了那么多的开源商城项目,最想推荐的还是这一个

穷极一生,我们究竟在追寻什么

真香!大佬总结了一份 Spring Boot 项目搭建模板

21届校招应届生Offer薪资曝光:年薪35万+,严重倒挂老员工是互联网行业常态?

单例模式双重校验锁_单例模式创建方式竟然有7种,你知道吗?_第1张图片       

如果觉得文章不错,可以在文末点个赞,点个在看单例模式双重校验锁_单例模式创建方式竟然有7种,你知道吗?_第2张图片

你可能感兴趣的:(单例模式双重校验锁)