深入理解JUC:第一章:volatile的三大特性
JUC是什么?
是java.util.concurrent并发包
什么是并发?什么是并行?
并发:多个线程访问同一个资源。像秒杀一样。
并行:就是你泡方便面,你一边烧热水,一边拆调料包。各种事情同时进行。
volatile是什么?
是java虚拟机提供的轻量级同步机制
volatile的三大特性?
- 保证可见性(遵守JMM的可见性)
- 不保证原子性
- 禁止指令重排
JMM(java内存模型)是什么?
是一组规则,规范,是一个虚的概念,不真实存在。它定义了程序中的变量访问方式。
JMM的三大特性?
- 可见性
- 原子性
- 有序性
JMM同步的规定有那些?
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁钱,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
什么是主内存?什么是自己的工作内存?
主内存:就是你买电脑选择8G内存,这个就是你的主内存,也是你new 一个对象存放的地方,是共享内存区域,所有线程都可以访问,java内存模型中规定所有不了存储在主内存里。
工作内存:是JVM在每个线程创建的时候为线程创建一个工作内存,工作内存是每个线程的私有数据区域。
线程对变量的操作是在工作内存中还是主内存中进行的?
JVM运行的实体是线程,线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存,不能直接操作主内存的变量,而工作内存中存储的是主内存中的变量副本,所以不同线程之间无法访问对方的工作内存,要通过主内存来完成。
JMM内存模型的可见性如何理解?
现在我要对student对象中age年龄进行赋值,有三个线程t1,t2,t3同时对age进行赋值,这三个线程会从主内存中拷贝一份副本,到自己的工作空间。
t1最先进行赋值,将age改为37,并写回主内存
这个时候其他现在还不知道主内存的值改变了,需要及时通知,这个及时通知就是JMM内存模型的可见性。接着t2和t3知道我手上这个值已经不是最新的值了,需要重新去主内存拷贝最新值,简单来说就是只要主内存的值改变了就通知线程让它重新拷贝。或者可以打个比方:只要我在微信群了@所有人,发红包,指令是“我是最帅的”,群里的其他人都知道我发红包了,让他们全部人回复“我是最帅的”就可以领取红包。
可见性的代码验证说明
没有加volatile时的代码
package com.javaliao.backstage;
class MyData{
int number = 0;
public void changeData(){
this.number = 100;
}
}
/**
* 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存
*/
public class Demo {
//主线程main,程序入口
public static void main(String[] args) {
//创建对象,number在主内存为0
MyData myData = new MyData();
//线程AAA
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程AAA执行");
//让他执行3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先将number为0从主内存中拷贝到工作内存中,然后在工作内存中操作number,改为100后写回主内存
myData.changeData();
System.out.println(Thread.currentThread().getName()+"\t 查看AAA线程的工作内存中number的值:"+myData.number);
},"AAA").start();
//继续执行main线程
while (myData.number == 0){
//只要main中的工作内存的number为0,main线程就一直在这里等待循环,直到number值不再为0
}
System.out.println(Thread.currentThread().getName()+"\t maing线程已经感知到了number从0变为100,可见性被触发");
}
}
控制台打印:
可以看见程序一直再循环,main的number一直没有改变,线程AAA和main之间没有通信,main没有人通知它
添加volatile时的代码,只改动MyData
class MyData{
volatile int number = 0;
public void changeData(){
this.number = 100;
}
}
控制台打印:
只要是加了volatile的变量,及时通知main线程number变量改变了,main线程中主内存拷贝到工作内存。
volatile不保证原子性如何理解?
先上代码理解:
package com.javaliao.backstage;
class MyData{
volatile int number = 0;
public void changeData(){
number ++;
}
}
/**
* 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存
*/
public class Demo {
//主线程main,程序入口
public static void main(String[] args) {
//创建对象,number在主内存为0
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
//创建20个线程
new Thread(()->{
//一个线程执行1000次加一的操作
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二,说明还有线程没有执行完任务,继续执行上面的代码
while (Thread.activeCount() > 2){
Thread.yield();
}
//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的值不是20000
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);
}
}
每次执行结果都不同,控制台打印结果如下:
由此可见volatile的使用线程不安全
为什么volatile不保证原子性?
举例:当线程1,线程2,线程3同时拿到主内存中的number=0,并且在工作内存中进行加一时,这个时候各个线程的工作内存里的变量要写回主内存,线程1在写回主内存这一个过程中因为cpu线程资源被抢占,挂起了,没有来的及通知其他线程number的值已经改为1了,线程2就将自己之前从主内存拷贝的number=0的变量(还没有更新number=1的变量)进行加一(这个时候理想性是number=2再写回主内存),而实际情况是线程2的工作内存中的变量number=1写回主内存,将线程1中已经写回主内存的number=1的覆盖了,导致数值丢失。
先解释一下number++这个命令:
MyData.java->MyData.class->JVM字节码
number++这个命令在JVM字节码被拆分成三个指令:
- 执行getfield拿到原始值
- 执行iadd进行加一的操作
- 执行putfield把累加的值写回主内存
javap -c里的是java字节码,这个是汇编底层原始命令
volatile不保证原子性,怎么解决原子性?
加synchronized,但是影响并发
package com.javaliao.backstage;
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int number = 0;
public synchronized void changeData(){
number++;//加一
}
}
/**
* 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存
*/
public class Demo {
//主线程main,程序入口
public static void main(String[] args) {
//创建对象,number在主内存为0
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
//创建20个线程
new Thread(()->{
//一个线程执行1000次加一的操作
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二继续执行上面的代码,
while (Thread.activeCount() > 2){
Thread.yield();
}
//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的数量不是20000
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);
}
}
使用AtomicInteger原子整型
package com.javaliao.backstage;
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void changeData(){
atomicInteger.getAndIncrement();//加一
}
}
/**
* 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存
*/
public class Demo {
//主线程main,程序入口
public static void main(String[] args) {
//创建对象,number在主内存为0
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
//创建20个线程
new Thread(()->{
//一个线程执行1000次加一的操作
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二继续执行上面的代码,
while (Thread.activeCount() > 2){
Thread.yield();
}
//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的数量不是20000
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.atomicInteger);
}
}
什么是JMM的有序性?
数据依赖性怎么理解?就是先有你爸再有你。
什么是指令重排?
假设我写的第20行代码,执行的时候不一定会从第一行执行到第20行,打个比方:参加高考做卷子,出题人给的题目,你不一定会从第一题做到最后一题,你可能会先把会的写了,其他有难度的题目最后写。
代码案例1:
再多线程环境下语句执行有1234和2134以及1324三种顺序,语句4不能重排后变成第一个,原因是什么说的数据依赖性,变量要先声明再使用。
案例2:
可以看到变量的值有二套,很恐怖的好吧,所以volatile需要禁止指令重排,确认最终变量的值。
案例3:
在多线程环境下指令重排,会导致二种结果:一个是0+5=5,一个是1+5=6
正常单线程环境下会执行语句1再执行语句2最后执行语句3,结果打印为5
多线程环境下指令重排了先执行语句2再执行语句3最后执行语句1,结果打印为6
很恐怖的好吧,数据的一致性不能保证,所以volatile需要禁止指令重排。
volatile禁止指令重排小总结
单例模式在多线程环境下可能存在安全问题
单线程下的单例模式:
package com.javaliao.backstage;
class MyData{
private static MyData myData = null;
private MyData(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static MyData getInstance(){
if(myData == null){
myData = new MyData();
}
return myData;
}
}
public class Demo {
//主线程main,程序入口
public static void main(String[] args) {
System.out.println(MyData.getInstance() == MyData.getInstance());
System.out.println(MyData.getInstance() == MyData.getInstance());
}
}
控制台打印正确:
多线程下再使用这样的单例模式:
package com.javaliao.backstage;
public class Demo {
private static Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
demo = new Demo();
}
return demo;
}
//主线程main,程序入口
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
控制台打印错误:
本来应该打印一次的,结果10个线程打印了5次,这就有问题了
有人会说加一个synchronized
public class Demo {
private static Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static synchronized Demo getInstance(){
if(demo == null){
demo = new Demo();
}
return demo;
}
//主线程main,程序入口
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
控制台打印正确:
但是这个不好,synchronized是重量型的,数据一致型得到保证了,但是影响并发
那怎么解决这个问题呢?
这个时候先介绍DCL,后面再一点点讲解。
什么是DCL双关检锁机制?
加锁前后都进行一次判断。
直接上代码理解,DCL(双关检锁机制)代码:
package com.javaliao.backstage;
public class Demo {
private static Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
synchronized (Demo.class){
if(demo == null){
demo = new Demo();
}
}
}
return demo;
}
//主线程main,程序入口
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
为什么要加二层判断呢?更加牢固一些
打个比方:你上厕所,你确认没有人了,然后再进去,进去了把门插上,再推一下门,推不动确认安全后该干啥干啥。
在多线程环境下使用DCL(双关检锁机制)是否就百分百OK呢?
不是,DCL(双关检锁机制)不一定线程安全,在多线程环境下,JMM中的有序性会让指令出现重排,让执行顺序发送变化,不能保证百分百。加入volatile可以禁止指令重排
单例模式volatile分析
在多线程环境下,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,造成线程安全问题。
所以加上volatile才可以保证百分百ok
public class Demo {
private static volatile Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
synchronized (Demo.class){
if(demo == null){
demo = new Demo();
}
}
}
return demo;
}
//主线程main,程序入口
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}