设计模式笔记(一):单例模式

  单例模式可以说是设计模式中最简单的设计模式之一了。顾名思义,单例模式指的是一个类只提供一个固定的单个实例,大家共用该实例。

  单例模式代码实现步骤:

  1、私有化类的构造方法

  2、提供私有静态实例变量

  3、提供公共静态方法使其返回私有实例变量

  基础的实现代码如下:

 1 public class Singleton {
 2 
 3     //私有化静态实例变量
 4     private static Singleton instance;
 5 
 6     //私有化构造方法,保证其他类无法创建该类的新对象
 7     private Singleton() {
 8     }
 9 
10     public static Singleton getInstance() {
11         //判断实例是否存在,若不存在则创建一个新对象并返回
12         if (instance == null) {
13             //创建对象时,打印一下
14             System.out.println("创建了一个实例");
15             instance = new Singleton();
16         }
17         return instance;
18     }
19     
20 }

  上述代码乍一看貌似没有什么问题,但是当处于多线程情况下,问题就会暴露出来。接下来编写main方法进行测试:

 1 public class Singleton {
 2 
 3     //私有化静态实例变量
 4     private static Singleton instance;
 5 
 6     //私有化构造方法,保证其他类无法创建该类的新对象
 7     private Singleton() {
 8     }
 9 
10     public static Singleton getInstance() {
11         //判断实例是否存在,若不存在则创建一个新对象并返回
12         if (instance == null) {
13             //创建对象时,打印一下
14             System.out.println("创建了一个实例");
15             instance = new Singleton();
16         }
17         return instance;
18     }
19 
20     public static void main(String[] args) {
21         //开启10个线程,调用获取实例方法
22         for (int i = 0; i < 10; i++) {
23             new Thread(() -> {
24                 Singleton.getInstance();
25             }).start();
26         }
27     }
28 }

  程序运行结果如下(每次程序运行结果都可能不同):

 

   结果很明显,在多线程情况下,使用上述代码创建单例实例时,会造成创建多个实例的问题。原因就在于上述代码并没有加锁,导致代码第12行,对instance判断为空时,可能存在多个线程同时执行到这一步,各个线程都认为实例没有被创建,于是又各自执行了初始化创建实例的代码(代码第15行)。

  问题的原因找到了,那么就好解决了,以下提供几种解决方法。

解决方法一:使用synchronized关键字修饰getInstance方法

  使用synchronized关键字修饰getInstance方法,将其变为同步方法,即意味着该方法在某个线程首次进入该方法执行完成之后,其他线程才能进入该方法,而当其他线程再次进入该方法时,instance实例已经被首次进入该方法的线程初始化好了,于是就避免了产生重复创建对象的问题。示例代码如下:

 1 public class Singleton {
 2 
 3     //私有化静态实例变量
 4     private static Singleton instance;
 5 
 6     //私有化构造方法,保证其他类无法创建该类的新对象
 7     private Singleton() {
 8     }
 9 
10     //添加了synchronized修饰该方法,使其变成同步方法
11     public static synchronized Singleton getInstance() {
12         //判断实例是否存在,若不存在则创建一个新对象并返回
13         if (instance == null) {
14             //创建对象时,打印一下
15             System.out.println("创建了一个实例");
16             instance = new Singleton();
17         }
18         return instance;
19     }
20 
21     public static void main(String[] args) {
22         //开启10个线程,调用获取实例方法
23         for (int i = 0; i < 10; i++) {
24             new Thread(() -> {
25                 Singleton.getInstance();
26             }).start();
27         }
28     }
29 }

  上述代码仅仅在第11行定义方法处添加了synchronized关键字修饰该方法,程序运行结果如下:

 

 

  结果表明多线程情况下,该代码实现了真正的单例变量的创建。

  注:实际情况下,只有第一次执行该方法时,才需要同步,后续的其他线程调用该方法时,此时的同步是没有必要的。另外synchronized作为重量级锁,在同步方法时会带来一定的性能影响。

 

解决方法二:使用“双重检查加锁”方法

  使用双重检查,首先判断实例是否已经被创建,若没有被创建则开始对对象加锁实现同步。这样的话,只会在首次创建实例时进行同步。

 1 public class Singleton {
 2 
 3     //私有化静态实例变量
 4     private static volatile Singleton instance;
 5 
 6     //私有化构造方法,保证其他类无法创建该类的新对象
 7     private Singleton() {
 8     }
 9 
10     public static Singleton getInstance() {
11         //判断实例是否存在,若不存在则创建一个新对象并返回
12         if (instance == null) {
13             //对Singleton.class对象加锁
14             synchronized (Singleton.class) {
15                 if (instance == null) {
16                     //创建对象时,打印一下
17                     System.out.println("创建了一个实例");
18                     instance = new Singleton();
19                 }
20             }
21         }
22         return instance;
23     }
24 
25     public static void main(String[] args) {
26         //开启10个线程,调用获取实例方法
27         for (int i = 0; i < 10; i++) {
28             new Thread(() -> {
29                 Singleton.getInstance();
30             }).start();
31         }
32     }
33 }

  请注意上述代码第4行,变量声明时使用了volatile修饰,这是为了保证多线程之间,某个对该变量的修改永远会对其他线程可见,这里涉及到happens-before原则还有重排序的知识,就不展开讨论了,大家有兴趣的可以自行百度看一下。

  注:在1.4以及更早版本的java中,很多JVM对于volatile的实现会导致双重检查加锁的失效,所以如果想使用双重检查加锁,请保证java版本至少在1.5及以上!

 

解决方法三:基于类初始化的解决方案

  JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案,这个方案被称之为Initialization On Demand Holder idiom)。代码示例如下:

 1 public class Singleton {
 2 
 3     //私有化构造方法,保证其他类无法创建该类的新对象
 4     private Singleton() {
 5     }
 6 
 7     //私有静态内部类
 8     private static class SingletonHolder {
 9         //直接新建一个实例对象
10         private static final Singleton INSTANCE = new Singleton();
11     }
12 
13     public static Singleton getInstance() {
14         return SingletonHolder.INSTANCE;
15     }
16 
17     public static void main(String[] args) {
18         //开启10个线程,调用获取实例方法
19         for (int i = 0; i < 10; i++) {
20             new Thread(() -> {
21                 Singleton.getInstance();
22             }).start();
23         }
24     }
25 }

  可以简单理解为,类对象在初始化时也会存在一个锁,该锁可以保证对类对象静态变量的写入对后续其他线程可见。

  个人推荐使用解决方法二或者解决方法三。

  参考资料:

  《java并发编程的艺术》

  《HeadFirst设计模式》

 

posted @ 2020-09-01 13:16  yjry-th  阅读(156)  评论(0编辑  收藏  举报