转载

Java设计模式系列之单例设计模式

Hello,大家好,距离上次写博客是2018年1月26号,算了下,有8个月没写博客了。这里给大家道个歉,因为我换了工作,现就职在深圳一家公司,换了城市,加上工作上的一些事,所以一直抽不开身,2个月前不是太忙的时候,一直想着写点什么,可又找不到感觉了,所有就慢慢吞吞的,今天下定决心,写点渣渣也要写。由于博主最近准备把设计模式这一块好好整一整,所以就从最简单的 单例模式 开始,这个单例模式,说简单也简单,说难也难。有点生疏,大家看的时候,如果觉得写的不好,多多包含,OK,进入正题,文章结构:

  1. 单例模式简介
  2. 饿汉式单例模式
  3. 懒汉式线程安全单例模式

1. 单例模式简介

所谓的单例模式,其实大家都知道,就是在应用程序中的某个类,无论在任何时间想拿到这个类的事例,都拿到的是唯一一个实例,那么问题来了,什么叫唯一的一个实例,说的再通俗点,就是拿到的那个指针(C,C++中叫指针,Java中叫引用对象)地址是唯一的。大家通过 new 关键字new几次new出来的对象,那肯定不是唯一的了。

2. 饿汉式单例模式

上代码之前,先说一下,单例模式的几个要点:

  • 构造函数私有化(上面说过了,代码中new出来的地址不唯一,那肯定要私有化,不能让用户代码去new)
  • 单例类提供方法或者单例对象

OK,上代码了,因为太简单了,不知道怎么再展开了。

public final class Student {
  // 注意这里是私有化的
  private Student() {}
  // 注意这里是私有化的
  private static final Student INSTANCE = new Student();
 // 暴露出去的方法
  public static Student getInstance() {
    return INSTANCE;
  }
}
复制代码

贼鸡儿简单,想拿Student对象的时候,直接Student.getInstance(); 不存在什么线程安全问题,因为类内部的static变量会在类加载的时候直接创建出来。你要 想整个静态代码快去初始化INSTANCE变量 ,其实也是一样一样的。这里就不写了。其实就是利用是static变量和static代码快在类加载时直接加载执行原理。

3. 懒汉式线程安全单例模式

其实对于绝大多出场景,上面的饿汉已经绝对够用了。比如Spring框架中的bean,默认情况下就是单例的,就是直接给你new出来,然后丢在内存里,你要@Autowire的时候,直接给你。但这里有个小问题,有些类的初始化非常耗时,比如数据库链接,Redis链接等,这种网络IO操作。很有可能因为网络原因导致很耗时,在类被加载而他的实例还没有被使用的时候,上面的饿汉模式显然是不太合适的,如果这种耗时比较多的饿汉单例比较多的话,影响应用程序的启动时间。So,我们的懒汉上场了,OK,Code:

public final class Student {

  private Student() {}
  
  private static  Student INSTANCE = null;
  
  public static Student getInstance() {
   //Mark 1 
    if(INSTANCE==null){
        INSTANCE= new Student();
    }
    return INSTANCE;
  }
}
复制代码

代码也是贼鸡儿简单,在getInstance()中判断一下INSTANCE是不是null,如果是null(第一次调用)就初始化,下一次不是Null,返回旧的那个。写到这里,有点并发编程经验的小伙伴就知道了,在 Mark 1 的位置,当有多个线程同时进入的话,会有两个线程同时进入if()代码快,那么就糟糕了,INSTANCE会被初始化多次。不是线程安全的。接下来重头戏,我会演示几种线程安全的懒汉式单例模式:

(a) synchronized 方法 保证线程同步

直接上代码:

public final class Student {

  private Student() {}
  
  private static  Student INSTANCE = null;
  // Mark 2 
  public static synchronized Student getInstance() {
   //Mark 1 
    if(INSTANCE==null){
        INSTANCE= new Student();
    }
    return INSTANCE;
  }
}
复制代码

其实很简单,就是在getInstance()方法上加了synchronized关键字,这里synchronized关键字就不展开讲了,其实就是锁住了整个getInstance()方法,保证线程同步,这样当有多个线程进入这个方法时,会以Student.class为锁,只有一个方法先进去,然后后面的线程再进去的时候,INSTANCE已经不是null了,就进入不了if代码块。所以可以保证if代码块在多线程的情况下只进入一次,也就是说Student类只被实例化一次。

(b) synchronized 代码块 双重锁检查(推荐)

上面的synchronized锁住方法,其实是阔以的,但大家都知道synchronized关键字锁住方法的效率还是有点小问题的,毕竟每次调用这个方法都加锁,想想都很不爽 。效率不是很高,我就不多说了,所以这里推荐使用的是synchronized关键字的 双重锁检查 方式,代码如下:

public final class Student {

  private Student() {}

  // Mark 1 
  private static volatile  Student INSTANCE = null;

  public static Student getInstance() {
    // Mark 2 
    if(INSTANCE == null)
      synchronized (Student.class){
        // Mark 3 
        if(INSTANCE==null){
          INSTANCE= new Student();
        }
      }
    return INSTANCE;
  }
}
复制代码

好了,来分析下这个代码,先看所谓的双重锁检查的Mark 2 和 Mark 3 ,如果没有Mark 2 的话,其实和在方法上加synchronized关键字的效果是一样一样的,效率比较差,每次进这个方法后,都加一波锁。如果没有Mark 3 的话,大家想一想,当有两个线程同时走到Mark 2 的位置,这时两个线程都进入第一个If代码快,然后在synchronized关键字的时候被锁住一个线程,另一个进去了,然后INSTANCE被初始化了,那么当这个线程出来后,另外一个被锁住的线程进去之后,如果没有Mark 3 的话,直接执行INSTANCE= new Student(); 又实例化了一个Student,这显然不是单例模式了。所有,Mark 2 和 Mark 3 的位置的if判断都是不能少的,这也就是所谓的双重锁检查了。这样即能保证Student类只被实例化一次,又能保证在安全的实例化后,后续getInstance()的时候不走有锁的代码了,是不是很完美。大家好好体会一下。最后值得一说的是,在Mark 1 的位置,必须保证这个INSTANCE变量是volatile 类型的,其实是为了保证线程可见性,保证第一个进入线程后的赋值操作,在后面的线程进入后,能够看到,也就是说Mark 3 位置能看到。给大家一个参考文章,解释为什么要volatile关键字的。 单例模式中用volatile和synchronized来满足双重检查锁机制

(c ) 静态内部类方式

public final class Student {


  private Student() {}

  public static Student getInstance() {
  
        return StudentInner.INSTANCE;
  }

  private static class StudentInner {
  
    private static final Student INSTANCE =
        new Student();
  }
}
复制代码

其实就是利用了静态内部类的加载顺序问题,(静态内部类的加载顺序),只有在调用StudentInner.INSTANCE的时候,静态内部类才被加载,INSTANCE变量才会被实例化,而且,类的加载肯定是线程安全的,不用考虑volatile和synchronized的问题。有意思不?!哈哈。

4. 结语

好了,单例模式其实就差不多了,网上也有很多相似的文章,我其实就是做了个总结,加了点自己的理解,没什么技术含量,后面给大家写其他设计模式的时候可能有点技术含量,结合实际的案例,比如哪些框架里用到了,这个单例模式,最典型的就是Spring容器里的Bean了,要是再要举例的话,那就是System.getSecurityManager()了,这个也是单例的。这个类是管理Java Application的权限的,不怎么用,我们一般都是运行容器时管理权限,很少在代码里去控制。 Over,Have a good day !

原文  https://juejin.im/post/5bbb1e06e51d450e4d3011f7
正文到此结束
Loading...