并发编程(二):全视角解析volatile

一、目录

  1、引入话题-发散思考

  2、volatile深度解析

  3、解决volatile原子性问题

  4、volatile应用场景

二、引入话题-发散思考

public class T1 {
     /*volatile*/ boolean running=true;

     public void m(){
          System.out.println(Thread.currentThread().getName()+":start!");
           while(running){
                /*try {
                     TimeUnit.MINUTES.sleep(2);
                } catch (Exception e) {
                     e.printStackTrace();
                }*/
           }
          System.out.println(Thread.currentThread().getName()+":end!");
     }

     public static void main(String[] args) {
           T1 t=new T1();

           new Thread(()->t.m(),"t").start();
           try {
                TimeUnit.SECONDS.sleep(1);
           } catch (Exception e) {
                e.printStackTrace();
           }
           t.running=false;
     }
}
运行结果:
无volatile:
t:start!

有volatile:
t:start!
t:end!

含有volatile是期望的结果,那为什么不添加volatile会产生这种情况呢?

再谈Java内存模型:

在虚拟机中,堆内存用于存储共享数据(实例对象),堆内存也就是这里说的主内存。

每个线程将会在堆内存中开辟一块空间叫做线程的工作内存,附带一块缓存区用于存储共享数据副本。那么,共享数据在堆内存当中,线程通信就是通过主内存为中介,线程在本地内存读并且操作完共享变量操作完毕以后,把值写入主内存。

解析无volatile:

  • 根据上述java内存模型可知,最开始running=true在主存中,开启线程A,线程会把主存的running=true复制一份写入工作内存的共享变量副本中。
  • 当我们改变running=false,在主存中已经发生改变。
  • 线程A一直在工作状态,没有空闲时间去知道主存的情况,而是一直在读本地内存的共享变量副本,也就一直running=true,取而代之也会产生上述情况。

三、volatile深度解析

那为什么含有volatile就能及时刷新工作内存呢?它有什么作用呢?

volatile:可见性(一个线程修改共享变量以后,立马会被其他线程知可见)、禁止重排序。

1、什么是可见性?

虚拟机的happens-before中的volatile规则:volatile变量写操作先于读操作,一个线程去读取volatile变量,另一个线程去写volatile变量,那么volatile变量的写操作优先。

  • 根据上述java内存模型可知,最开始running=true在主存中,开启线程A,线程会把主存的running=true复制一份写入工作内存的共享变量副本中。
  • 当我们改变running=false,在主存已经发生改变。
  • 就在这时,当主存与工作内存发生不一致的时候,工作内存的共享变量会失效,那么工作内存就会去主存刷新一遍共享变量,所以running=false,自然就执行下面的代码啦!

2、什么是禁止重排序?

先谈有序性:

int a=1;
int b =3;
int c=a*b;

在虚拟机中,执行上述代码,一定是按照上述顺序执行吗?那可不一定,像a=1,b=3的顺序完全可能先执行b=3,a=1,这被称为重排序;但是c=a*b一定在a=1,b=3后面,这被成为有序性。

再谈重排序:

//线程1:
context = loadContext();  //语句1
inited = true;            //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

假设上述代码在单线程中,谈过重排序不会对代码造成什么影响,但是我们看这一段代码。

语句1与语句2并没有太多的依赖关系,参考有序性例子,那么他们就可以重排序,那么可能语句2执行先于语句1,假设在线程1中语句2执行完就刷新inited到主存,还没等语句1执行呢?线程2就执行起来,一看inited=true,挑出循环,执行下面的代码,context=null就报出空指针,显然这是不能被虚拟机允许的。

所以,volatile明确规定禁止重排序,意思就是context=loadContext必须先于inited执行。

在虚拟机中设定了有序性,也就是前面谈到的happens-before原则。如果不满足上该原则的情况下,虚拟机是可以自由的重排序的,下面附录此规则。

3、先行发生原则(happens-before)

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

四、解决volatile原子性问题

1、volatile能解决原子性问题吗?什么是原子性呢,本不想解释,为了读者能够更透彻理解,再解释一下。

原子性:只有一个线程访问共享数据,也就是当线程A访问一个代码块的时候,其他线程全部堵塞,只有等代码块全部执行完,才能被其他线程访问共享数据。

public class T2 {
     volatile int count=0;

     public void m(){
           for(int i=0;i<1000;i++)
                count++;
     }

     public static void main(String[] args) {
           T2 t=new T2();
           List<Thread> threads=new ArrayList<Thread>();

           for(int i=0;i<10;i++){
                threads.add(new Thread(()->t.m(),"thread-"+i));
           }

           threads.forEach((o)->o.start());

           //等待所有线程都执行完
           threads.forEach((o)->o.yield());

           System.out.println("count:"+t.count);
     }
}
运行结果:
count:8710 //每次都不一样。

 2、为什么加了volatile还是不能得到预期结果呢?因为它只保证了可见性,不能保证原子性。what?

再回忆java内存模型:

3、那怎么解决呢?

方式一:synchronized,jvm对synchronized进行了很大的优化,所以效率也没有想象中那么低。

public class T3 {
     int count=0;

     public synchronized void m(){
           for(int i=0;i<1000;i++)
                count++;
     }

     public static void main(String[] args) {
           T3 t=new T3();
           List<Thread> threads=new ArrayList<Thread>();

           for(int i=0;i<10;i++){
                threads.add(new Thread(()->t.m(),"thread-"+i));
           }

           threads.forEach((o)->o.start());

           //等待所有线程都执行完
           threads.forEach((o)->o.yield());

           System.out.println("count:"+t.count);
     }
}

方式二:ReentrantLock,跟synchronized的作用差不多。

public class T5 {
     ReentrantLock lock=new ReentrantLock();
     int  count=0;

     public void m(){
           lock.lock();
           for(int i=0;i<1000;i++)
                count++;
           lock.unlock();
     }

     public static void main(String[] args) {
           T4 t=new T4();
           List<Thread> threads=new ArrayList<Thread>();

           for(int i=0;i<10;i++){
                threads.add(new Thread(()->t.m(),"thread-"+i));
           }

           threads.forEach((o)->o.start());

           //等待所有线程都执行完
           threads.forEach((o)->o.yield());

           System.out.println("count:"+t.count);
     }

}

方式三:AtomicInteger原子类

public class T4 {
     AtomicInteger count=new AtomicInteger(0);

     public void m(){
           for(int i=0;i<1000;i++)
                count.getAndIncrement();
     }

     public static void main(String[] args) {
           T4 t=new T4();
           List<Thread> threads=new ArrayList<Thread>();

           for(int i=0;i<10;i++){
                threads.add(new Thread(()->t.m(),"thread-"+i));
           }

           threads.forEach((o)->o.start());

           //等待所有线程都执行完
           threads.forEach((o)->o.yield());

           System.out.println("count:"+t.count);
     }
}

五、volatile应用场景

说到这里,读者可能就已经懵逼了,这也有问题那也有问题,那我们什么时候用它呢?

volatile是基于synchronized提出的效率优化手段,但是它是不能代替synchronized的。

状态标记量:

一般volatile共享变量,不要用于数据计算,最好去标记一些状态值,比如前面说的running=true。

volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

九、版权声明

  作者:邱勇Aaron

  出处:http://www.cnblogs.com/qiuyong/

  您的支持是对博主深入思考总结的最大鼓励。

  本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,尊重作者的劳动成果。

  参考:深入理解JVM、马士兵并发编程、并发编程实践

     volatile关键字解析:http://www.importnew.com/18126.html

时间: 2024-10-19 21:00:45

并发编程(二):全视角解析volatile的相关文章

Java并发编程(六):volatile关键字解析(转载)

原文转载自:http://www.cnblogs.com/dolphin0520/p/3920373.html volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我

聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类

这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. 这12个类能够分为三组: 1. 普通类型的原子变量 2. 数组类型的原子变量 3. 域更新器 普通类型的原子变量的6个, 1. 当中AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference分别相应boolean, int,  long, object完毕主要的原子操作 2. AtomicMarkableRe

【Java并发编程二】同步容器和并发容器

一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchornized. 另一个是Collections类中提供的静态工厂方法创建的同步包装类. 同步容器都是线程安全的.但是对于复合操作(迭代.缺少即加入.导航:根据一定的顺序寻找下一个元素),有时可能需要使用额外的客户端加锁进行保护.在一个同步容器中,复合操作是安全

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

干货:Java并发编程必懂知识点解析

本文大纲 并发编程三要素 原子性 原子,即一个不可再被分割的颗粒.在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) 可见性 当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值. 2. 线程的五大状态 创建状态 当用 new 操作符创建一个线程的时候 就绪状态 调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度

干货:Java并发编程必懂知识点解析(内附面试题)

本文大纲 1.并发编程三要素 原子性 原子,即一个不可再被分割的颗粒.在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) 可见性 当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值. 2. 线程的五大状态 创建状态 当用 new 操作符创建一个线程的时候 就绪状态 调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的

Java 并发编程(二):如何保证共享变量的原子性?

线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心扑在工作上面,所以处理的蛮得心应手,心态也一直保持的不错:但有了副业之后,心态就变得像坐过山车一样.副业收入超过主业的时候,人特别亢奋,像打了鸡血一样:副业迟迟打不开局面的时候,人就变得惶惶不可终日. 仿佛我就只能是个单线程,副业和主业并行开启多线程模式的时候,我就变得特别没有安全感,尽管整体的收入

并发编程(二)

五.线程的概述 5.1 什么是线程 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程就是进程 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线 流水线的工作需要电源,电源就相当于cpu 所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上执行的单位. 多线程(即多个控制线程)的概念是,在一个进程中存在朵儿控制线程,多个控制线程共

并发编程二

一.守护进程 主进程创建守护进程,守护进程的主要的特征为:①守护进程会在主进程代码执行结束时立即终止:②守护进程内无法继续再开子进程,否则会抛出异常. 实例: from multiprocessing import Process import time def foo(): print('starting123') time.sleep(1) print('endig123') def bar(): print('starting456') time.sleep(3) print('endin