多线程知识梳理(4),当我们谈到volatile的时候,我们在谈什么?

首先要明白每一个线程都是有自己单独的内存区域来执行操作的,也就是有单独的计数器,单独的局部变量等。多线程之间的共享对象,如果在多线程环境下不做特殊处理是极易出问题的。现在主要说的是线程交互之间的可见性。

那什么是可见性呢,简单来说就是纸某个线程修改共享变量的指令对其他线程来说都可见的,它反映的是指令执行的实时透明度。

下面说一个高并发场景中可能出现的问题,先看一段代码

首先要明白每一个线程都是有自己单独的内存区域来执行操作的,也就是有单独的计数器,单独的局部变量等。多线程之间的共享对象,如果在多线程环境下不做特殊处理是极易出问题的。现在主要说的是线程交互之间的可见性。
那什么是可见性呢,简单来说就是纸某个线程修改共享变量的指令对其他线程来说都可见的,它反映的是指令执行的实时透明度。
下面说一个高并发场景中可能出现的问题,先看一段代码

class LazyInitDemo {
    private static TransactionService service = null;
    
    public static TransactionService getTransacationService(){
          if (service == null) {
              synchronized (this) {
                   if (service == null) {
                        service = new TransactionService();
                   }
              }
          }
         return service;
    }
    
}

因为 service = new TransactionService(); 不是原子操作。使用者在调用getTransacationService()方法的时候,有可能会得到一个没有被初始化的对象。 简单来说就是,假设某个线程执行new Transaction()时,构造方法还没有被调用,编译器仅仅为改对象分配了内存空间,并设置为默认值,若此时另外一个线程调用该方法,由于service != null ,但是此时service对象其实还没有被赋予真正有效的值,从而无法获取到正确有效的service单例对象。

这就是著名的双重检查锁定问题,对象引用在没有同步的情况下进行读操作,导致用户可能会获取未构造完成的对象。

对于这种问题,一种简单的解决方案使用volatile关键字修饰目标属性,这样service就限制编辑器对它和它相关的读写操作,对它的读写操作进行指令重排,确定对象实例化之后才返回引用。

volatile 解决的多线程共享变量的可见性问题,类似于 synchronize,但是不具备 synchronized 的互斥性。所以对 volatile 变量的操作并非都具有原子性!!!这个可以通过以下的实例代码做测试:

public class VolatileDemo {

    private static volatile long count = 0L;
    private static final int NUMBER = 10000;

    public static void main(String[] args) {
        Thread subThread = new SubtractThread();
        subThread.start();
        for (int i = 0; i < NUMBER; i++) {
            count++;
        }
        // 等待线程运行结束
        while (subThread.isAlive()){}

        System.out.println(count);
    }

    private static class SubtractThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < NUMBER; i++) {
                count--;
            }
        }
    }
}

多次执行之后,打印结果基本都不为 0,只有在 count++和 count--处加锁,才能获得预期为 0 的结果

for (int i = 0; i < NUMBER; i++) {
     synchronized(this) {
          count--;
     }         
}

在实际业务中,如何清晰的判断一写多读的场景显得尤为重要。如果不确定共享变量是否会被多个线程并发写,保险的做法是使用同步代码块来实现
线程的同步。另外因为所有的操作都需要同步给内存变量,所以 volatile 一定会使现成的执行速度变慢,所以定义要慎重。

猜你喜欢

转载自www.cnblogs.com/joimages/p/12806564.html