java多线程4:volatile关键字

上文说到了 synchronized,那么就不得不说下 volatile关键字了,它们两者经常协同处理多线程的安全问题。

volatile保证可见性

那么volatile的作用是什么呢?

在jvm运行时刻内存的分配中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,

线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,

然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,

在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

 

 


read and load 从主存复制变量到当前工作内存
use and assign  执行代码,改变共享变量值 
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现

注意这些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,

不会产生对应的变化,所以计算出来的结果会和预期不一样,对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。

如何解决缓存不一致的问题?

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。

因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),

从而使得只能有一个CPU能使用这个变量的内存。比如一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,

那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。

这样就解决了缓存不一致的问题。但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。

它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,

因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

看一个例子:

public class MyThread15 extends Thread
{
    private boolean isRunning = true;

    public boolean isRunning()
    {
        return isRunning;
    }

    public void setRunning(boolean isRunning)
    {
        this.isRunning = isRunning;
    }
    
    public void run()
    {
        System.out.println("进入run了");
        while (isRunning == true){}
        System.out.println("线程被停止了");
    }
}

  

@Test
    public void test15() throws InterruptedException {

    try{
        MyThread15 a = new MyThread15();
        a.start();
        Thread.sleep(1000);
        a.setRunning(false);
        System.out.println("已赋值为false");
    }
    catch (InterruptedException e){
        e.printStackTrace();
    }
        a.join();
    }

  执行结果:

进入run了
已赋值为false

  可以看待,明明已经将 isRunning设置为false了,但是“线程被停止了”还是没有被执行。

当我们给isRunning加上关键字volatile后,执行结果:

进入run了
已赋值为false
线程被停止了

  可见volatile关键字可以保证共享变量在多线程之间的可见性。

 

volatile不保证原子性


下面看个demo,定义一个线程类,将count值++  10000次。

public class MyDomain16 {

    private volatile int count = 0;

    public MyDomain16() {
    }

    public void count() {
        for (int i=0; i<10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
        System.out.println(Thread.currentThread().getName() + ": count=" + count);
    }
} 
public class Mythread16 extends Thread {

    private MyDomain16 myDomain16;

    public Mythread16(MyDomain16 myDomain16) {
        this.myDomain16 = myDomain16;
    }

    public void run() {
        myDomain16.count();
    }
}

@Test
public void test16() throws InterruptedException {
    MyDomain16 myDomain16 = new MyDomain16();

    Mythread16 a = new Mythread16(myDomain16);
    Mythread16 b = new Mythread16(myDomain16);
    a.start();
    b.start();
    a.join();
    b.join();
}

  执行结果:

Thread-1: count=17913
Thread-0: count=17919

  

  可以看出两个线程总共应该增加2万次,但是count值并没有等于20000,说明自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

 

当我们给count方法加上synchronized之后

public class MyDomain16 {

    private volatile int count = 0;

    public MyDomain16() {
    }

    public synchronized void count() {
        for (int i=0; i<10000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
        System.out.println(Thread.currentThread().getName() + ": count=" + count);
    }
}

  执行结果:

Thread-0: count=10000
Thread-1: count=20000

  

总结:

关键字synchronized和volatile进行一下比较:

1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。

随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。

2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。

3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。

4)关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

  线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

 

参考文献

1:《Java并发编程的艺术》

2:《Java多线程编程核心技术》

  

posted @ 2021-12-14 10:54  让我发会呆  阅读(125)  评论(0编辑  收藏  举报