1

单例设计模式

一般某些的类只需要一个就够了,重复创建他们将花费大量资源时,可以使用单例模式。比如某些工厂类、工具类。

饿汉式

静态常量
 /**
  * @author objcfeng
  * @description 饿汉式——静态常量
  * @Step: 
  * 1.私有化构造器
  * 2.创建私有类实例
  * 3.公有方法返回类实例
  * @date 2020/10/13
  */
 public class HungryMan01 {
  private static final HungryMan01 instance=new HungryMan01();
  private HungryMan01() {
  }
  public static HungryMan01 getInstance(){
  return instance;
  }
 }

步骤:

  1. 私有化构造器阻止外部代码通过构造器构建类实例;
  2. 创建私有类实例的成员变量声明为静态常量
  3. 通过一个公有的静态方法getInstance提供类实例。

注意:提供类实例的方法一定是静态的,不然无法访问到这个方法。

静态代码块
 /**
  * @author objcfeng
  * @description 饿汉式——静态代码块
  * @date 2020/10/13
  */
 public class HungryMan02 {
  private static final HungryMan02 instance;
  private HungryMan02() {
  }
  static {
  instance=new HungryMan02();
  }
  public static HungryMan02 getInstance(){
  return instance;
  }
 }

步骤差不多,只是把创建私有类实例的过程放在静态代码块中执行罢了。

饿汉式都是线程安全的。

懒汉式

非线程安全的懒汉式
 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 @NotThreadSafe
 public class LazyMan {
  private static LazyMan instance=null;
 ​
  private LazyMan() {
  }
  public LazyMan static getInstance(){
  if (instance==null){
  instance=new LazyMan();
  }
  return instance;
  }
 }

懒汉式解决了饿汉式类加载时就创建了实例而不管实例是否被用到的问题。

但是这里的懒汉式是存在线程安全问题的,在并发环境下,当线程A执行到if (instance==null)结果为true,并进入if的执行代码块中,但在执行实例化操作前,CPU分配给了了线程B,线程B开始执行,判断if (instance==null)为true,并执行实例化操作生成类实例B。然后CPU重新分配给线程A,A已经通过判断了,所以又执行了一次实例化操作生成类实例A。这时便有两个类实例了。

代码验证:

 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 ​
 @NotThreadSafe
 public class LazyMan {
  private static LazyMan instance=null;
 ​
  private LazyMan() {
  }
  public static LazyMan getInstance() throws InterruptedException {
  if (instance==null){
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan();
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  LazyMan lazyMan1=LazyMan.getInstance();
  System.out.println(lazyMan1);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  LazyMan lazyMan2=LazyMan.getInstance();
  System.out.println(lazyMan2);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }
 ​

输出

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.LazyMan@43d83830
 创建型.懒汉式.LazyMan@6218fb2e

由之前的分析可以看出,懒汉式不是线程安全的关键有两点,一是在线程A执行完判断未执行实例化时就有线程B进入方法;二是当线程A重新获得CPU资源后没有再次判断if内的条件还是否为true;解决二者之一,就能解决懒汉式非线程安全的问题了。

首先来尝试解决二号问题,首先再次判断在if内再使用一次if肯定是不行的,原因还是会在通过判断的时候可能发生CPU资源的切换。我记得在解决虚假唤醒的时候曾将 if 改为while来再次执行判断,可以一试:

 @NotThreadSafe
 public class LazyMan02 {
  private static LazyMan02 instance=null;
 ​
  private LazyMan02() {
  }
  public static LazyMan02 getInstance() throws InterruptedException {
  while (instance==null){
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if(Thread.currentThread().getName().equals("A")){
  Thread.yield();//让线程变为就绪态
  }
  System.out.println(Thread.currentThread().getName()+"执行实例化");
  instance=new LazyMan02();
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  LazyMan02 lazyMan1= LazyMan02.getInstance();
  System.out.println(lazyMan1);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  LazyMan02 lazyMan2= LazyMan02.getInstance();
  System.out.println(lazyMan2);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }

输出

 A进入方法getInstance()...
 B进入方法getInstance()...
 B执行实例化
 A执行实例化
 创建型.懒汉式.LazyMan02@4cc3632
 创建型.懒汉式.LazyMan02@7245297c

发现不行,看来使用while再次进行判断只有在使用等待、唤醒(wait、notify)时才能起效果。

第一个问题解决起来就简单了,添加同步,在线程A没出来前限制其他线程进入就完事了。见下章。

线程安全的懒汉式
 @ThreadSafe
 public class LazyMan03 {
  private static LazyMan03 instance=null;
 ​
  private LazyMan03() {
  }
  public static synchronized LazyMan03 getInstance() throws InterruptedException {
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if (instance==null){
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan03();
  }
  return instance;
  }
 }

如上加同步(synchronized)就完了。

输出:

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.非线程安全.LazyMan03@4cc3632
 创建型.懒汉式.非线程安全.LazyMan03@4cc3632

可见两个类实例是一样的。

虽然解决起来简单但是缺点也很大,在并发大的时候,每个线程都要等进入getInstance()内的方法执行完后才有可能进入方法开始获取实例。性能很低。

双重检查
 /**
  * @author objcfeng
  * @description 非线程安全的懒汉式
  * @date 2020/10/13
  */
 ​
 @ThreadSafe
 public class LazyMan04 {
  private static volatile LazyMan04 instance=null;
 ​
  private LazyMan04() {
  }
  public static LazyMan04 getInstance() throws InterruptedException {
  System.out.println(Thread.currentThread().getName()+"进入方法getInstance()...");
  if (instance==null){
  synchronized (LazyMan04.class){
  if (instance==null){
  if(Thread.currentThread().getName().equals("A")){
  TimeUnit.SECONDS.sleep(1);
  }
  instance=new LazyMan04();
  }
  }
  }
  return instance;
  }
 ​
  public static void main(String[] args) {
  new Thread(()->{
  try {
  DoubleCheck doubleCheck= DoubleCheck.getInstance();
  System.out.println(doubleCheck);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"A").start();
  new Thread(()->{
  try {
  DoubleCheck doubleCheck= DoubleCheck.getInstance();
  System.out.println(doubleCheck);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  },"B").start();
  }
 }

输出:

 A进入方法getInstance()...
 B进入方法getInstance()...
 创建型.懒汉式.线程安全.LazyMan04@6218fb2e
 创建型.懒汉式.线程安全.LazyMan04@6218fb2e

双重检查,顾名思义就是使用了两次if判断,当类实例还未创建出来时,线程通过第一个判断进入同步代码块,进行第二次判断和创建类实例,保证了线程在判断后和执行创建前不会有其他线程进入代码块,保证了线程安全性。

另外,在类实例创建出来后,所有线程都不会再进入同步代码块,保证了效率。

为什么要使用volatile?

因为new不是原子操作。

 public class Test {
  public static void main(String[] args) {
  new Object();
  }
 }

使用javac命令编译.java文件为.class文件

javac -encoding UTF-8 Test.java

再使用javap命令反编译.class文件

 javap -c Test

 D:WorkSpaceJavaJava设计模式MDsrc创建型双重检查>javac -encoding UTF-8 Test.java
 ​
 D:WorkSpaceJavaJava设计模式MDsrc创建型双重检查>javap -c Test.class
 Compiled from "Test.java"
 public class 创建型.双重检查.Test {
  public 创建型.双重检查.Test();
  Code:
  0: aload_0
  1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  4: return
 ​
  public static void main(java.lang.String[]);
  Code:
  //创建对象实例分配内存
  0: new           #2                  // class java/lang/Object
  //复制栈顶地址,并将其压入栈顶
  3: dup
  //调用构造器方法,初始化对象
  4: invokespecial #1                  // Method java/lang/Object."<init>":()V
  7: pop
  8: return
 }
线程1线程2
t1分配内存
t2变量赋值
t3 判断对象是否为null
t4 由于对象不为null,访问该对象
t5初始化对象

如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。(参考自https://www.cnblogs.com/zhuifeng523/p/11360012.html

volatile 作用

正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。

  1. 保证可见性。使用 volatile定义的变量,将会保证对所有线程的可见性。
  2. 禁止指令重排序优化。

由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

静态内部类

 /**
  * @author objcfeng
  * @description 单例—静态内部类
  * @date 2020/10/16
  */
 public class StaticInnerClass {
  private StaticInnerClass() {
  }
 ​
  private static class Singleton{
  private static final StaticInnerClass INSTANCE=new StaticInnerClass();
  }
  public static StaticInnerClass getInstance(){
  return Singleton.INSTANCE;
  }
 }

实现步骤:

  1. 私有化构造器;
  2. 私有静态内部类创建外部类的静态常量实例;
  3. 外部类公有方法返回这个实例。

实现原理:外部类加载时不会立即加载内部类,内部类不加载就不会初始化INSTANCE,实现懒加载。只有当第一次调用getInstance()方法时,内部类才会加载。

缺点:由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。一般单例模式都在静态内部类与双重检查(DCL Double Check Lock)之间抉择。

枚举

 /**
  * @author objcfeng
  * @description 枚举实现单例
  * @date 2020/10/16
  */
 public class EnumSingleton {
  private EnumSingleton() {
  }
  enum E{
  INSTANCE;
  private EnumSingleton singleton;
  E() {
  singleton=new EnumSingleton();
  }
  }
  public static EnumSingleton getInstance(){
  return E.INSTANCE.singleton();
  }
 }

实现步骤:

  1. 私有化构造函数
  2. 建立只有一个枚举实例的枚举类,枚举类有外部类的实例变量声明,枚举的构造函数中初始化外部类实例。
  3. 外部类公有方法返回这个实例。

优点:线程安全、防止反序列化创建新的对象。


coffee
21 声望2 粉丝

慢慢来,会很快。