您的位置:首页 > 其它

用 volatile 和 synchronized 实现单例

2021-01-13 20:44 274 查看

现在,关于 `volatile` 和 `synchronized` 的用法仍然有很多争议。`volatile` 并不是什么新技术,然而理解它对于每个专业 Java 开发者来说很重要。另外,关于这个主题我没有找到好的总结文章和示例,促使我写下本文。


简单地说,`volatile` 关键字标记的变量具备"可见性",无论是读操作还是写操作都会强制刷新到主内存,确保多个线程可以看到变量修改后的值。另一方面,`synchronized` 也提供了**可见性**,通过添加互斥锁让所有受到加锁保护的代码与主内存同步。这样可以防止某个线程读取时其他线程正在更新该对象,造成读到的状态与实际不一致。


单例实现:使用 volatile Bean 模式


让我们通过一个延迟加载单例来帮助理解。示例采用双重检查锁定并实现了“volatile bean 模式”,代码如下:


> 译注:双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示"(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。-- WikiPedia


```java
public class MutableSingleton {
   private static volatile MutableSingleton INSTANCE;
   private static final Object mutex = new Object();
   private volatile boolean someFlag;
   // 这个单例包含更多可变状态

   private MutableSingleton(boolean someFlag) {
       this.someFlag = someFlag;
   }

   public static MutableSingleton getInstance() {
       MutableSingleton singleton = INSTANCE;
       if (singleton != null)
           return singleton;
       synchronized (mutex) {
           if (INSTANCE == null)
               INSTANCE = new MutableSingleton(false);
           return INSTANCE;
       }
   }

   public boolean isSomeFlag() {
       return someFlag;
   }

   public void setSomeFlag(boolean someFlag) {
       this.someFlag = someFlag;
   }
}
```


上面是一个可变的多线程单例实现,其中`INSTANCE` 和 `someFlag` 变量都标记为 `volatile`,但后者没有包含在 `synchronized` 代码块中。对单例进行的修改会立刻被其他线程看到(即可见性)。下面是测试用例:


```java
public class MutableSingletonTest {
   private long counter = 0;
   public Object[] execute(Object... arguments) {
       final Timer timer = new Timer();
       timer.schedule(new TimerTask() {
           public void run() {
               MutableSingleton.getInstance().setSomeFlag(true);
               System.out.println("Timer interrupted main thread ...");
               timer.cancel();
           }
       }, 1000);
       while (!MutableSingleton.getInstance().isSomeFlag()) {
           counter++;
       };
       System.out.println("Main thread was interrupted by timer ...");
       return new Object[] { counter, MutableSingleton.getInstance().isSomeFlag() };
   }
   private class Worker implements Runnable {
       @Override
       public void run() {
           Object[] result = execute();
           System.out.println(result[0]+"/"+result[1]);
       }
   }
   public static void main(String[] args) throws InterruptedException {
       MutableSingletonTest volatileExample = new MutableSingletonTest();
       Thread thread1 = new Thread(volatileExample.new Worker(), "Worker-1");
       thread1.start();
       Thread.sleep(5000);
   }
}
```


让我们快速了解整个程序。首先,`main()` 方法(第29行)创建了 `thread1`,在线程里执行 `execute()` 方法(第24行)。`execute()` (第5行)会新建 TimerThread(第7行)调度执行。接着,`thread1` 会循环等待(第14行),直到 `timer` 线程设置 `someFlag` 为 true(第9行)。使用 `MutableSingleton` 执行结果如下:


```shell
Timer interrupted main thread ...
Main thread was interrupted by timer ...
804404197/true
```


`timer` 线程与 `thread1` 正确同步,这是因为 `volatile` 在 `MutableSingleton` 上发挥了作用。


让单例失效


删除 `MutableSingleton` 中 `volatile` 关键字再次运行测试,结果应该是这样的(至少在我的机器上是这样的)。这意味着线程协调失效了:


```shell
Timer interrupted main thread ...
```


上面的运行结果表明 `thread1` 无法看到 `someFlag` 修改后的值。`timer` 线程把 `someFlag` 设置为 `true`,但由于 `volatile` 关键字已经删除,`thread1` 看不到更新后的值。


不使用 volatile 会怎么样?


现在让我们做一些别的测试:如果去掉 `MutableSingleton` 上的 `volatile` 关键字,给 `someFlag` 加上 `synchronized` 会怎么样呢?修改单例中的状态会对线程可见吗?下面是修改后的 `MutableSingleton` 类:


```java
public class MutableSingletonSynchronized {
   private static MutableSingletonSynchronized INSTANCE;
   private static final Object mutex = new Object();
   private boolean someFlag;

   // 这个单例包含更多可变状态
   private MutableSingletonSynchronized(boolean someFlag) {
       this.someFlag = someFlag;
   }

   public static MutableSingletonSynchronized getInstance() {
       MutableSingletonSynchronized singleton = INSTANCE;
       if (singleton != null)
           return singleton;
       synchronized (mutex) {
           if (INSTANCE == null)
               INSTANCE = new MutableSingletonSynchronized(false);
           return INSTANCE;
       }
   }

   public synchronized boolean isSomeFlag() {
       return someFlag;
   }

   public synchronized void setSomeFlag(boolean someFlag) {
       this.someFlag = someFlag;
   }
}
```


在上面的代码中,访问 `someFlag` 已标记为 `synchronized` 并且删掉了 `volatile` 关键字。再次执行测试,控制台的输出如下。注意,这次没有使用 `volatile`。


```shell
Timer interrupted main thread ...
Main thread was interrupted by timer ...
60113040/true
```


看起来线程运行一切正常。原因在于访问 `someFlag` 的方法加上了 `synchronized` 关键字,这样本地线程与主内存能正确同步。这里有一个问题:如果采用 `synchronized` 访问,还需要 `volatile` 吗?答案很明确:视情况而定。


volatile 的隐藏特性


只要包含可变状态,采用双重检查锁定模式创建单例,都建议在 `INSTANCE` 字段上使用 `volatile`。因此对于上面的示例,虽然没有使用 `volatile` 线程也能够正常工作,但是应该加上 `volatile` 关键字。这里用到了 `volatile` 的另一个“特性”,有时候被称作“一次性安全发布”。该特性解决了双重检查锁定模式带来的问题,造成问题的原因如下:


由于缺乏同步机制,既可能看到其他线程写入后的新值,也可能看到对象之前的旧值。


这是什么意思?如果不使用 `volatile`,JIT 编译器可能会改变 `INSTANCE` 字段赋值操作(`MutableSingleton` 代码第17行),重新对赋值语句排序。例如,先把静态 `INSTANCE` 字段设为包含默认值的实例,再把可变成员字段 `someFlag` 设为构造函数传入的初始值。情况不凑巧时,会出现后面的线程先进入 `null` 检查(`MutableSingleton` 第13行),这时 `INSTANCE` 的值已经不是 `null`。此时,返回给客户端的实例包含的是不完整的默认值。由于可能出现这种复杂情况,推荐下面规则:


在应用双重检查锁定模式创建单例时,务必使用 `volatile`。


理由:


当单例 `INSTANCE` 字段加上 `volatile` 关键字时,编译器不会对该字段赋值重新排序。


不可变对象


对于不可变单例,情况又有一些不同。如果单例是不可变对象,那么即使不使用 `volatile`,双重检查锁定模式的延迟初始化也能正常运行。[这是因为][1]读写不可变对象都是原子操作。因此,这里要小心确认,引用的对象是否真的不可变:


[1]:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html


对于不可变对象,在双重检查锁定模式中不强制使用 `volatile`。


volatile 没有互斥锁


使用 `volatile` 时还有一点需要注意:尽管 `volatile` 能够确保所有更改对线程都可以见,但并没有像 `synchronized` 那样增加互斥锁。这意味着代码并非线程安全。


```java
private static volatile int nextSerialNumber = 0;
public static int generateNextSerialNumber() {
  return nextSerailNumber++;
}
```


修改 `nextSerialNumber` 对所有线程可见。但是由于(++)实际上执行了读写两个操作,当多个线程调用 `generateNextSerialNumber()` 访问同一个地址时,可能会出现数据竞争。可以通过添加 `synchronized` 解决:


```java
private static int nextSerialNumber = 0;
public static synchronized int generateNextSerialNumber() {
  return nextSerailNumber++;
}
```


只要访问 `nextSerialNumber` 字段标记为 `synchronized`,就不必再使用 `volatile`。


结合 volatile 与 synchronized


综上所述,一个线程安全、性能优良的完整单例模式看起来应该像下面这样:


```java
public class MutableSingletonComplete {
   private static volatile MutableSingletonComplete INSTANCE;
   private static final Object mutex = new Object();
   private volatile boolean someFlag;
   private volatile int counter;
   // 这个单例包含更多可变状态
   private MutableSingletonComplete(boolean someFlag) {
       this.someFlag = someFlag;
   }
   public static MutableSingletonComplete getInstance() {
       MutableSingletonComplete singleton = INSTANCE;
       if (singleton != null)
           return singleton;
       synchronized (mutex) {
           if (INSTANCE == null)
               INSTANCE = new MutableSingletonComplete(false);
           return INSTANCE;
       }
   }
   public boolean isSomeFlag() {
       return someFlag;
   }
   public void setSomeFlag(boolean someFlag) {
       this.someFlag = someFlag;
   }
   public int getCounter() {
       return counter;
   }
   public synchronized void incrementCounter() {
       counter++;
   }
}
```


总结:


  • 对 `INSTANCE` 字段使用 `volatile` 关键字,应用一次性安全发布;

  • 对单例属性使用 `volatile` 关键字,保持可见性;

  • 如果修改状态不是原子操作,即使该字段声明为 `volatile`,也需要使用 `synchronized` 关键字,使用互斥锁;

  • 如果单例中的 `volatile` 属性是对象引用,要么该对象也是线程安全的实现,要么对象本身不可变。这样实现会更加完善。


好了,今天的困惑已经够多了,让我们喝杯咖啡。示例代码可以在[这里][2]找到。


希望本文的示例对系统化了解 `volatile` 有所帮助。文中内容难免有错漏,欢迎评论、提问、抛出你的想法。


[2]:https://github.com/nschlimm/playground-java8/tree/master/src/main/java/org/projectbarbel/playground/revisitevolatile



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: