ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

java多线程4:volatile关键字

2021-12-14 11:02:03  阅读:124  来源: 互联网

标签:count java 变量 volatile 内存 线程 多线程 public


上文说到了 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多线程编程核心技术》

  

标签:count,java,变量,volatile,内存,线程,多线程,public
来源: https://www.cnblogs.com/yxy-ngu/p/15686786.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有