java多线程:volatile的深度理解

背景

首先通过以下两段代码对比理解volatile的作用:

代码段1

程序通过创建50个线程访问静态共享变量v,对v实现累加并输出。

/**
 * volatile测试
 *
 * @author zhuhuix
 * @date 2020-05-05
 */
public class VolatileThread1 implements Runnable {
    public  static int v=0 ;
    @Override
    public void run() {
        System.out.println("线程:" 
        + Thread.currentThread().getName() 
        + "获取的静态值为:" + (++v));
    }

    public static void main(String[] args) {
        VolatileThread1 volatileThread1 = new VolatileThread1();
        for (int i = 0; i < 50; i++) {
            new Thread(volatileThread1).start();
        }

    }
}

输出结果如下:通过多个线程访问静态值,每个线程会形成本地缓存副本,也就是说缓存中的变量并未得到同步,造成线程不安全。
在这里插入图片描述
再看代码段2:

代码段2
/**
 * volatile测试-加上volatile修饰静态变量v
 *
 * @author zhuhuix
 * @date 2020-05-05
 */
public class VolatileThread1 implements Runnable {

    public volatile   static int v=0 ;

    @Override
    public void run() {
        System.out.println("线程:" + Thread.currentThread().getName() + "获取的静态值为:" + (++v));
    }

    public static void main(String[] args) {

        VolatileThread1 volatileThread1 = new VolatileThread1();
        for (int i = 0; i < 50; i++) {
            new Thread(volatileThread1).start();

        }

    }
}

通过反复验证,各个线程在访问该变量时,资源得到了同步,也就是说线程是安全的。
在这里插入图片描述

实现原理

用volatile对变量进行修饰时,实际上底层做了以下两件事:
1)将当前处理器缓存的数据写回到系统内存。
2)同时这个写回内存的操作会使在其他缓存了该内存地址的数据无效

声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

也就是说通过volatile的修饰让变量的读写得到了同步,保证了线程的安全。

volatile的应用

从上面volatile的实现原理,我们知道了用volatile的修饰的静态共享变量,可以保证同步功能,利用这个特性,我们可以实现线程间的通信。

/**
 * volatile测试-利用volatile的特性实现线程通信
 *
 * @author zhuhuix
 * @date 2020-05-05
 */
public class VolatileThread {
    public volatile static boolean flag = true;
    public static Long count = 0L;

    public static void main(String[] args) {

        Runnable r1 = () -> {
            System.out.println("开始执行线程1");
            while (flag) {
                count++;
                if (count % 100000000 == 0) {
                    System.out.println("正在执行线程1!!!"+count);
                }
            }
            System.out.println("线程1被线程2打断执行!!!");
        };
        new Thread(r1).start();

        Runnable r2 = () -> {

            System.out.println("开始执行线程2,5秒后停止执行线程1");
            try {
                Thread.sleep(5000);
                flag = false;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(r2).start();
    }
}

输出结果如下:线程2执行5秒后,置静态变量flag标志为false,通过volatile的特程,同步到线程1,线程1获取该信息后,终止线程运行。
在这里插入图片描述
以下用图来表示线程1和线程2之间的通信原理:

在这里插入图片描述

volatile与synchronized的主要区别

  • volatile关键字解决的是内存可见性的问题
  • synchronized关键字解决的是执行控制的问题
    针对以上两个区别,我们可以改造一下代码段2的程序:
代码段3
/**
 * volatile测试-将volatile替换成synchronized
 *
 * @author zhuhuix
 * @date 2020-05-05
 */
public class VolatileThread2 implements Runnable {

    int v = 0;

    public synchronized void Increment() {
        v++;
        System.out.println("线程:" + Thread.currentThread().getName() + "获取的静态值为:" + getV());
    }

    public synchronized int getV() {
        return v;
    }

    @Override
    public void run() {
        Increment();
    }

    public static void main(String[] args) {

        VolatileThread2 volatileThread2 = new VolatileThread2();
        for (int i = 0; i < 50; i++) {
            new Thread(volatileThread2).start();

        }

    }
}

代码段3与代码段2的执行效果是一样的,但原理截然不同:代码段3中当线程运行Increment时进行加锁,完成该方法时解锁,确保在同一时刻只有一个线程运行此方法,从而保证了资源的同步与线程的安全性。
在这里插入图片描述

©️2020 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值