Java多线程之volatile详解


目录:

  1. 什么是volatile?
  2. JMM内存模型之可见性
  3. volatile三大特性之一:保证可见性
  4. volatile三大特性之二:不保证原子性
  5. volatile三大特性之三: 禁止指令重排
  6. 小结

1. 什么是volatile?


答:volatile是java虚拟机提供的轻量级的同步机制(可以理解成乞丐版的synchronized)

特性有:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

理解volatile特性之一保证可见性之前要先理解什么是JMM内存模型的可见性

2. JMM内存模型之可见性


  1. JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

  2. JMM关于同步的规定:

    1. 线程解锁前,必须把共享变量的值刷新回主内存
    2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
    3. 加锁解锁是同一把锁
  3. 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量 的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成 后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线
    程间无法去访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

    4.图解:


即创建student对象age=25,每个线程自己的工作内存都会拷贝一份age = 25,当线程t1修改age=37后,需要把age=37写回主内存,然后主内存向其他线程分发最新的值。

volatile保证可见性特性也是如此

volatile三大特性之一:保证可见性


1. 结合代码理解volatile的可见性

代码

import java.util.concurrent.TimeUnit;class MyData{int number = 0;public void addTo60(){this.number = 60;}
}public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}myData.addTo60();System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);},"AAA").start();//第2个线程就是我们的main线程//如果number==0,那么一直在死循环,下面的输出打印不出来,//如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发while (myData.number==0){// main线程就一直在这里等待循环,直到number值不再为零。}System.out.println(Thread.currentThread().getName()+"\t mission is over");}
}

编译结果:

AAA线程已经把myData.number从0赋值为60,并且写回了主内存,但是对main线程不可见。所以main线程一直在傻傻的等while(myData.number==0),但实际真实值number=60了,

2. 当我们在number添加volatile修饰符,即volatile int number = 0;

代码:

import java.util.concurrent.TimeUnit;class MyData{//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。volatile int number = 0;public void addTo60(){this.number = 60;}
}public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}myData.addTo60();System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);},"AAA").start();//第2个线程就是我们的main线程//如果number==0,那么一直在死循环,下面的输出打印不出来,//如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发while (myData.number==0){// main线程就一直在这里等待循环,直到number值不再为零。}System.out.println(Thread.currentThread().getName()+"\t mission is over" +", main get number value:"+myData.number);}
}

编译结果:


4. volatile三大特性之二:不保证原子性


1.首先要知道原子性指的是什么意思?
  • 不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
  • 需要整体完整。要么同时成功,要么同时失败。

2.通过代码验证volatile不保证原子性

代码:

class MyData {//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。volatile int number = 0;public void addTo60() {this.number = 60;}//请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。public void addPlusPlus() {number++;}
}public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();for (int i = 1; i <= 20; i++) {new Thread(() -> {for (int j = 1; j <= 1000; j++) {myData.addPlusPlus();}}, String.valueOf(i)).start();}//需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少// >2 是因为后台有两个线程,1是main线程,2是GC线程。// 能最好的控制时间while (Thread.activeCount()>2){Thread.yield();  //礼让线程,退不执行。}System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData.number);}
}

编译结果:

多次测试,都没有20000,说明有值丢失,即不保证原子性

3.如何解决volatile不保证原子性的问题?

我们可以用java.util.concurrent.atomic包下的 AtomicInteger解决这个问题


具体使用如下:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;class MyData {   //MyData.java ==> MyData.class ==> JVM字节码//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。volatile int number = 0;public void addTo60() {this.number = 60;}//请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。public void addPlusPlus() {number++;}AtomicInteger atomicInteger = new AtomicInteger();public void addMyAtomic(){atomicInteger.getAndIncrement();}
}public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();for (int i = 1; i <= 20; i++) {new Thread(() -> {for (int j = 1; j <= 1000; j++) {myData.addPlusPlus();myData.addMyAtomic();}}, String.valueOf(i)).start();}//需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少// >2 是因为后台有两个线程,1是main线程,2是GC线程。// 能最好的控制时间while (Thread.activeCount()>2){Thread.yield();  //礼让线程,退不执行。}System.out.println(Thread.currentThread().getName()+"\t int type,finally number value:"+myData.number);System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type ,finally number value:"+myData.atomicInteger);}
}

编译结果:


4.关于volatile数字丢失的简单原理:


上图解释:

  • 比如拿回自己工作空间的时候都是3,+1后写回去的时候,正好被别的线程捷足先登,只能挂起,已经有线程把4写了回去,等再唤醒的时候再把4写回去就会造成丢值

5. number++在多线程下是不安全的,为什么不用synchronized?

因为synchronized是重锁,有更合适的就用更合适的,杀鸡焉用牛刀。


5. volatile三大特性之三: 禁止指令重排


1. 说指令重排之前,我们要知道什么是有序性?


可能会出现问题,如下:

2. volatile特性之三:禁止指令重排
  1. volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。我们先了解一个概念:内存屏障
3.内存屏障
  1. 内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

    • 保证特定操作的执行顺序,
    • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
  2. 由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能
    和这条MemoryBarrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作
    用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

6. 小结

  1. 工作内存与主内存同步延迟现象导致的可见性问题

    解:可以使用Synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

  2. 对于指令重排导致的可见性问题和有序性问题

    解:可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

Java多线程之volatile详解相关推荐

  1. java并发编程之Volatile详解

    前言 在Java中多个线程对公共变量的操作并不是直接在内存中操作的,每一个线程都会有一块自己的工作内存.线程会先从主内存中获取到变量的值到工作内存中进行修改在更新到主内存.假如有两个线程同时对某个变量 ...

  2. Java多线程之Synchronized详解

    一直以来对于Synchronized都比较迷惑,尤其还对于ReentrantLock并不了解他们之间的区别,今天闲来无事,学习了. 1,为什么要使用Synchronized 首先看Synchroniz ...

  3. java多线程之ThreadLoal详解

    一.ThreadLocal简介 多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时.为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步 同步一般是 ...

  4. 多线程之callable详解

    多线程之callable详解 面试有人会问:线程的实现方式有几种? 很多人可能回答:2种,继承Thread类,实现Runnable接口. 很多忽略了callable这种方式. 也许有人知道callab ...

  5. Java并发编程之CyclicBarrier详解

    简介 栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生.栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行.闭锁用于等待事件,而栅栏用于等待其他线程. CyclicBarrier ...

  6. Java并发编程之ConcurrentLinkedQueue详解

    简介 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法.使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两 ...

  7. Java并发编程之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  8. 多线程之ThreadPoolExecutor详解

    一.为什么使用ThreadPoolExecutor来创建线程池 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 因为线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解 ...

  9. 异步多线程之ThreadPool详解

    上一篇:异步多线程之Thread 下一篇:异步多线程之入Task 介绍 ThreadPool 是 .net 2.0 时代的产物,有了 Thread 为什么还会有 ThreadPool 呢?Thread ...

最新文章

  1. 类继承、组合和抽象类
  2. 汇编语言 利用栈 将数据逆序存放
  3. 使用约束控件创建界面
  4. php查询mysql返回大量数据结果集导致内存溢出的解决方法
  5. 我不藏了:7个技术体系、共100篇文章、总计1OO万字
  6. Final Project Proposal ——陈稳霖
  7. Macro版Property Generator辅助工具
  8. 送给那些渐渐远离的朋友(转载)
  9. 联想启天m420刷bios_联想启天m425装win7,联想启天m420改win7
  10. 深度好文| Redis面试全攻略
  11. 爬虫之模拟强智系统登录
  12. 女友让我每天半夜十二点给她发晚安?我用 Python 做了个定时发消息神器!怕她干嘛!
  13. c#实现浏览器端大文件分块上传
  14. V380固件自动升级失败修复过程
  15. mysql不识别生僻字_MySQL生僻字(不常用字)的完整解决方案
  16. 读书笔记: 当我谈跑步时,我谈些什么
  17. PhpExcel读取Excel表格中的数据
  18. [导入]一些博客聚合和书签网址
  19. Spring Boot 拦截器无效,不起作用
  20. Method threw 'java.lang.NullPointerException' exception. Cannot evaluate org.json.JSONObject.toSt...

热门文章

  1. 联合国隐私监督机构:大规模信息监控并非行之有效
  2. [转]SSH反向连接及Autossh
  3. 深入分析 Java 方法反射的实现原理
  4. Spark学习之Spark调优与调试(7)
  5. 代理模式之Java动态代理
  6. POJ - 3263 Tallest Cow(简单差分)
  7. java io字符输出流_JAVA IO 字符输入流与输出流总结说明
  8. python xlrd读取excel-使用Python xlrd模块读取Excel格式文件的方法
  9. UVA - 11694 Gokigen Naname(dfs)
  10. mysql 定时同步数据_MySQL数据同步之otter