Java
Java多线程

Java多线程 10 - 单例模式的线程问题

简介:单例模式的实现一般有两种:饿汉式和懒汉式。其中饿汉式的单例对象不会出现线程安全问题。

1. 单例模式的线程问题

单例模式的实现一般有两种:饿汉式和懒汉式。其中饿汉式的单例对象不会出现线程安全问题,它的代码如下:

  • public class HungerSingleton {
  • // 私有化构造方法
  • private HungerSingleton() {}
  • // 静态对象,类加载就会被初始化
  • private static HungerSingleton instance = new HungerSingleton();
  • // 直接返回创建好的静态实例
  • public static HungerSingleton getInstance() {
  • return instance;
  • }
  • }

饿汉式单例模式实现步骤为三步:

  1. 私有化构造方法,这样外界就无法通过构造方法创建对象。
  2. 声明一个静态变量,直接将其初始化为单例对象,它会在类加载的时候直接创建,因此不会出现线程安全问题。
  3. 实现一个方法,用于获取第二步中创建的静态变量。

饿汉式单例模式唯一的缺点是,即使还没有用到单例对象,类一加载就会创建单例对象,这会无形当中添加了内存的提前消耗。

懒汉式单例模式可以弥补饿汉式单例模式的缺点,但却存在安全问题。懒汉式单例模式演变步骤如下:

  1. 首先是不考虑线程安全性问题时实现的懒汉式单例模式:
  • public class LazySingleton {
  • // 私有化构造方法
  • private LazySingleton() {
  • }
  • // 仅仅声明,并不实例化
  • private static LazySingleton instance;
  • // 提供静态方法供外界获取单例对象
  • public static LazySingleton getInstance() {
  • // 对象为空才创建
  • if (instance == null)
  • instance = new LazySingleton();
  • return instance;
  • }
  • }

这种方式在多线程情况下,第10 ~ 11行是存在线程安全性问题的。

  1. 初步解决线程性安全问题,可以使用synchronized关键字:
  • public class LazySingleton {
  • // 私有化构造方法
  • private LazySingleton() {
  • }
  • // 仅仅声明,并不实例化
  • private static LazySingleton instance;
  • // 添加synchronized关键字
  • public static synchronized LazySingleton getInstance() {
  • // 对象为空才创建
  • if (instance == null)
  • instance = new LazySingleton();
  • return instance;
  • }
  • }

在获取单例对象的静态方法上添加synchronized关键字,可以直接解决第一种方式中存在的线程安全性问题。但synchronized同步方法的实现使用的是重量锁的模式,这种模式会影响程序性能。

  1. 改进后的懒汉式单例模式:
  • public class LazySingleton {
  • // 私有化构造方法
  • private LazySingleton() {
  • }
  • // 仅仅声明,并不实例化
  • private static LazySingleton instance;
  • public static LazySingleton getInstance() {
  • // 双重检查加锁,避免当instance已经不为null的时候还进行加锁操作
  • if (instance == null) {
  • // 将synchronized关键字用在代码块上
  • synchronized (LazySingleton.class) {
  • if (instance == null) {
  • instance = new LazySingleton();
  • }
  • }
  • }
  • return instance;
  • }
  • }

这种将synchronized只用在局部代码块的改进后的懒汉式单例模式似乎可以在解决线程安全性问题的前提下避免性能问题,但其实从虚拟机执行字节码的角度来看,还是存在一定的问题的,造成这个问题的原因即是:代码重排。

Java编译器在编译类文件为字节码文件时,会对某些代码指令进行重排,例如instance = new LazySingleton();这行代码,一般情况下,Java虚拟机执行的步骤应该是,首先申请一块内存,然后在这块内存中创建实例对象,然后将instance执行这块内存,这种情况是不会有问题的;但一旦经过指令重排,可能会在申请完内存后,直接将instance先指向这块内存,然后在这块内存中创建实例对象,这种情况下对instance == null的判断就会出现偏差,会引起线程安全性问题。

  1. 懒汉式单例模式最终版本,使用volatile关键字:
  • public class LazySingleton {
  • // 私有化构造方法
  • private LazySingleton() {
  • }
  • // 仅仅声明,并不实例化,添加volatile关键字,避免指令重排
  • private static volatile LazySingleton instance;
  • public static LazySingleton getInstance() {
  • // 双重检查加锁,避免当instance已经不为null的时候还进行加锁操作
  • if (instance == null) {
  • // 将synchronized关键字用在代码块上
  • synchronized (LazySingleton.class) {
  • if (instance == null) {
  • instance = new LazySingleton();
  • }
  • }
  • }
  • return instance;
  • }
  • }

使用volatile关键字,可以确保本条指令不会因编译器的优化而省略,且要求每次直接读值。 因此不会出现指令重排而导致线程安全性问题。