设计模式笔记(一):单例模式
单例模式可以说是设计模式中最简单的设计模式之一了。顾名思义,单例模式指的是一个类只提供一个固定的单个实例,大家共用该实例。
单例模式代码实现步骤:
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设计模式》