hi,你好!欢迎访问本站!登录
本站由网站地图腾讯云宝塔系统阿里云强势驱动
当前位置:首页 - 教程 - 杂谈 - 正文 君子好学,自强不息!

【并发编程】Volatile道理和运用场景剖析

2019-11-18杂谈搜奇网48°c
A+ A-

目次

  • 一个简朴列子
  • Java内存模子
    • 缓存不一致题目
    • 并发编程中的“三性”
  • 运用volatile来处置惩罚同享变量可见性
  • volatile和指令重排(有序性)
  • volatile和原子性
  • volatile运用场景
  • volatile运用总结
  • 参考

volatile是Java供应的一种轻量级的同步机制,在并发编程中,它也扮演着比较主要的角色。一个硬币具有两面,volatile不会形成上下文切换的开支,然则它也并能像synchronized那样保证一切场景下的线程平安。因而我们须要在适宜的场景下运用volatile机制。

我们先运用一个列子来引出volatile的运用场景。

一个简朴列子

public class VolatileDemo {

    boolean started = false;

    public void startSystem(){
        System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
        started = true;
        System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
    }

    public void checkStartes(){
        if (started){
            System.out.println("system is running, time:"+System.currentTimeMillis());
        }else {
            System.out.println("system is not running, time:"+System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        Thread startThread = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.startSystem();
            }
        });
        startThread.setName("start-Thread");

        Thread checkThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    demo.checkStartes();
                }
            }
        });
        checkThread.setName("check-Thread");
        startThread.start();
        checkThread.start();
    }

}

上面的列子中,一个线程来转变started的状况,别的一个线程不停地来检测started的状况,如果是true就输出体系启动,如果是false就输出体系未启动。那末当start-Thread线程将状况改成true后,check-Thread线程在实行时是不是能马上“看到”这个变化呢?答案是不肯定能马上看到。这边我做了许多测试,大多数状况下是能“感知”到started这个变量的变化的。然则偶然会存在感知不到的状况。请看下下面日记纪录:

上面的征象能够会让人比较疑心,为何有时刻check-Thread线程能感知到状况的变化,有时刻又感知不到变化呢?这个要从Java的内存模子提及。

Java内存模子

我们晓得,盘算机在实行递次时,每条指令都是在CPU中实行的。而实行指令历程当中,必将涉及到数据的读取和写入。递次运转历程当中的暂时数据是存放在主存(物理内存)当中的,这时刻就存在一个题目,由于CPU实行速率很快,而从内存读取数据和向内存写入数据的历程跟CPU实行指令的速率比起来要慢的多,因而如果任何时刻对数据的操纵都要经由过程和内存的交互来举行,会大大下降指令实行的速率。为了处置惩罚这个题目,“巨人们”就设想了CPU高速缓存。

下面举个列子来申明下CPU高速缓存的事情道理:

i = i+1;

当线程实行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU实行指令对i举行加1操纵,然后将数据写入高速缓存,末了将高速缓存中i最新的值革新到主存当中。

这个代码在单线程中运转是没有任何题目的,然则在多线程中运转就会有题目了。在多核CPU中,每条线程能够运转于差别的CPU中,因而每一个线程运转时有本身的高速缓存(对单核CPU来讲,实在也会涌现这类题目,只不过是以线程调理的情势来离别实行的)。本文我们以多核CPU为例,下面举个列子:

同时有2个线程实行上面这段代码,如果初始时i的值为0,那末从直观上看末了i的结果应该是2。然则现实能够不是如许。
能够存在下面一种状况:初始时,两个线程离别读取i的值存入各自地点的CPU的高速缓存当中,然后线程1举行加1操纵,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值照样0,举行加1操纵以后,i的值为1,然后线程2把i的值写入内存。终究结果i的值是1,而不是2。这就是有名的缓存一致性题目。一般称这类被多个线程接见的变量为同享变量。

缓存不一致题目

上面的列子申清楚明了同享变量在CPU中能够会涌现缓存不一致题目。为了处置惩罚缓存不一致性题目,一般来讲有以下2种处置惩罚要领:

  • 经由过程在总线加LOCK#锁的体式格局;
  • 经由过程缓存一致性协定;

这2种体式格局都是硬件层面上供应的体式格局。

在初期的CPU当中,是经由过程在总线上加LOCK#锁的情势来处置惩罚缓存不一致的题目的。由于CPU和其他部件举行通讯都是经由过程总线来举行的,如果对总线加LOCK#锁的话,也就是说壅塞了其他CPU对其他部件接见(如内存),从而使得只能有一个CPU能运用这个变量的内存。比方上面例子中 如果一个线程在实行 i = i +1,如果在实行这段代码的历程当中,在总线上发出了LCOK#锁的信号,那末只要守候这段代码完整实行终了以后,其他CPU才能从变量i地点的内存读取变量,然后举行响应的操纵。如许就处置惩罚了缓存不一致的题目。然则上面的体式格局会有一个题目,由于在锁住总线时期,其他CPU没法接见内存,致使效力低下

所以就涌现了缓存一致性协定。最着名的就是Intel 的MESI协定,MESI协定保证了每一个缓存中运用的同享变量的副本是一致的。它中心的头脑是:当CPU写数据时,如果发明操纵的变量是同享变量,即在其他CPU中也存在该变量的副本,会发出信号关照其他CPU将该变量的缓存行置为无效状况,因而当其他CPU须要读取这个变量时,发明本身缓存中缓存该变量的缓存行是无效的,那末它就会从内存从新读取。

经由过程上面临Java内存模子的解说,我们发明每一个线程都有各自对同享变量的副本拷贝,代码实行是对同享变量的修正,实在起首修正的是CPU中高速缓存中副本的值。而这个修正对其他线程是不可见的,只要当这个修正革新回主存中(革新的机遇不肯定)而且其他线程从新读取这个主存中的值时,这个修正才对其他线程可见。这个也就诠释了上面列子中的征象。check-Thread线程缓存了started的值是false,start-Thread线程将started副本的值转变成true后并没有立马革新到主存中去,所以当check-Thread线程再次实行时拿到的started值照样false。

并发编程中的“三性”

在正式讲volatile之前,我们先来诠释下并发编程中常常碰到的“三性”。

  1. 可见性
    可见性是指当多个线程接见同一个同享变量时,一个线程修正了这个变量的值,其他线程能够马上看得到修正的值。

  2. 原子性
    原子性是指一个操纵或许多个操纵要么悉数实行而且实行的历程不会被任何要素打断,要么就都不实行。

  3. 有序性
    有序性是指递次实行的递次根据代码的先后递次实行。

运用volatile来处置惩罚同享变量可见性

上面的列子中存在的题目是:start-Thread线程将started状况转变以后,check-Thread线程不能立马感知这个变化。也就是说这个同享变量的变化在线程之间是不可见的。那怎样来处置惩罚同享变量的可见性题目呢?Java中供应了volatile关键字这类轻量级的体式格局来处置惩罚这个题目的。volatile的运用异常简朴,只须要用这个关键字润饰你的同享变量就好了:

private volatile boolean started = false;

volatile能到达下面两个结果:

  • 当一个线程写一个volatile变量时,JMM会把该线程对应的本地内存中的变量值强迫革新到主内存中去;
  • 这个写会操纵会致使其他线程中的这个同享变量的缓存失效,从新去主内存中取值。

volatile和指令重排(有序性)

volatile另有一个特征:制止指令重排序优化。
重排序是指编译器和处置惩罚器为了优化递次机能而对指令序枚举行排序的一种手腕。然则重排序也须要恪守肯定划定规矩:

  1. 重排序操纵不会对存在数据依靠关联的操纵举行重排序
    比方:a=1;b=a; 这个指令序列,由于第二个操纵依靠于第一个操纵,所以在编译时和处置惩罚器运转时这两个操纵不会被重排序。

  2. 重排序是为了优化机能,然则不论怎样重排序,单线程下递次的实行结果不能被转变
    比方:a=1;b=2;c=a+b这三个操纵,第一步(a=1)和第二步(b=2)由于不存在数据依靠关联,所以能够会发作重排序,然则c=a+b这个操纵是不会被重排序的,由于须要保证终究的结果肯定是c=a+b=3。

重排序在单线程形式下是肯定会保证终究结果的正确性,然则在多线程环境下,能够就会出题目。照样用上面相似的列子:

public class VolatileDemo {

    int value = 1;
    private boolean started = false;

    public void startSystem(){
        System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
        value = 2;
        started = true;
        System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
    }

    public void checkStartes(){
        if (started){
            //关注点
            int var = value+1;  
            System.out.println("system is running, time:"+System.currentTimeMillis());
        }else {
            System.out.println("system is not running, time:"+System.currentTimeMillis());
        }
    }
}

上面的代码我们并不能保证代码实行到“关注点”处,var变量的值肯定是3。由于在startSystem要领中的两个复制语句并不存在依靠关联,所以在编译器举行代码编译时能够举行指令重排。也就是先实行
started = true;实行完这个语句后,线程立马实行checkStartes要领,此时value值照样1,那末末了在关注点处的var值就是2,而不是我们设想中的3。

运用volatile关键字润饰同享变量便能够制止这类重排序。若用volatile润饰同享变量,在编译时,会在指令序列中插进去内存屏蔽来制止特定范例的处置惩罚重视排序。volatile制止指令重排序也有一些划定规矩:

  • 当第二个操纵是voaltile写时,不论第一个操纵是什么,都不能举行重排序

  • 本地一个操纵是volatile读时,不论第二个操纵是什么,都不能举行重排序

  • 当第一个操纵是volatile写时,第二个操纵是volatile读时,不能举行重排序

volatile和原子性

volatile并非在一切场景下都能保证线程平安的。下面举个列子:

public class Counter {
    public static volatile int num = 0;
    //运用CountDownLatch来守候盘算线程实行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程举行累加操纵
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操纵
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //守候盘算线程实行完
        countDownLatch.await();
        System.out.println(num);
    }
}

上面的代码中,每一个线程都对同享变量num加了10000次,一共有30个线程,那末觉得上num的末了应该是300000。然则实行下来,大几率末了的结果不是300000(人人能够本身实行下这个代码)。这是由于何缘由呢?

题目就出在num++这个操纵上,由于num++不是个原子性的操纵,而是个复合操纵。我们能够简朴讲这个操纵明白为由这三步构成:

  • step1:从主存中读取最新的num值,并在CPU中存一份副本;
  • step2:对CPU中的num的副本值加1;
  • step3:赋值。

到场如今有两个线程在实行,线程1在实行到step2的时刻被阻断了,CPU切换给线程2实行,线程2成功地将num值加1并革新到内存。CPU又切会线程1继承实行step2,然则此时不会再去拿最新的num值,step2中的num值是已逾期的num值。

上面代码的实行结果和我们预期不符的缘由就是相似num++这类操纵并非原子操纵,而是分几步完成的。这些实行步骤能够会被打断。在中状况下volatile就不能保证线程平安了,须要运用锁同等步机制来保证线程平安。

volatile运用场景

 synchronized关键字是防备多个线程同时实行一段代码,那末就会很影响递次实行效力,而volatile关键字在某些状况下机能要优于synchronized,然则要注意volatile关键字是没法替换synchronized关键字的,由于volatile关键字没法保证操纵的原子性。一般来讲,运用volatile必需具有以下2个前提:

  • 对变量的写操纵不依靠于当前值;
  • 该变量没有包含在具有其他变量的稳定式中。

下面枚举两个运用场景

  • 状况标记量(本文中代码的列子)
  • 两重搜检(单例形式)
class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

volatile运用总结

  • volati是Java供应的一种轻量级同步机制,能够保证同享变量的可见性和有序性(制止指令重排),volatile的完成道理是基于处置惩罚器的Lock指令的,这个指令会使得对变量的修正立马革新回主内存,同时使得其他CPU中这个变量的副本失效;
  • volatile关于单个的同享变量的读/写(比方a=1;这类操纵)具有原子性,然则像num++这类复合操纵,volatile没法保证其原子性;
  • volatile的运用场景不是许多,运用时须要深切斟酌下当前场景是不是实用volatile(记着“对变量的写操纵不依靠于当前值”、“该变量没有包含在具有其他变量的稳定式中”这两个运用前提)。罕见的运用场景有多线程下的状况标记量和两重搜检等。

参考

  • https://www.cnblogs.com/dolphin0520/p/3920373.html
  • https://www.cnblogs.com/chengxiao/p/6528109.html
  选择打赏方式
微信赞助

打赏

QQ钱包

打赏

支付宝赞助

打赏

  移步手机端
【并发编程】Volatile道理和运用场景剖析

1、打开你手机的二维码扫描APP
2、扫描左则的二维码
3、点击扫描获得的网址
4、可以在手机端阅读此文章
未定义标签

本文来源:搜奇网

本文地址:https://www.sou7.cn/282403.html

关注我们:微信搜索“搜奇网”添加我为好友

版权声明: 本文仅代表作者个人观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。请记住本站网址https://www.sou7.cn/搜奇网。

发表评论

选填

必填

必填

选填

请拖动滑块解锁
>>