文章目录
Java并发知识以及volatile关键字
Java并发的重要性在于最大限度提高计算资源的效率。
并发是多线程中交替的访问同一个资源,同一个资源可以实CPU资源,可以是内存资源,该资源的特点只能同一时刻只能一个线程进行访问。
临界资源和临界区
临界资源
一般指内存资源,一个时刻只能有一个线程进行访问,一个线程正在使用临界资源的时候,另一个线程是不能使用,临界资源是非可剥夺资源。即JVM也无法阻止这种资源的独占行为
临界区
是一个线程中访问 临界资源的程序片段
,不是内存资源。这就是临界区和临界资源的区别。
临界区使用原则: 空闲让进 ,忙则等待 , 有限等待 , 让权等待
空闲让进
临界资源空闲一定让线程进入,不发生“互斥礼让”行为 。
忙则等待
临界资源正在使用时,外面的线程就需要等待
有限等待
线程进入临界区的时间时有限的,不会发生“饿死”
的情况。
饿死
:进入到临界区的线程,有一定时间限制,不可能让此线程一直执行下去,如果一直执行,那么其他需要等待此资源的线程就会一直等待下去,执行不了 即为 饿死现象。
让权等待
线程不能进入
到临界区时 应该让出CPU的使用,一面进程陷入忙等状态
线程安全
在单线程下和多线程执行下,最终得到的结果是相同
的。
并发特性
Java的并发模型中围绕并发过程中如何处理原子性,可见性,有序性问题
原子性
如果一个操作是不可分割的,即称之为原子性。
int a = 10; //1 10赋值给线程工作内存中的变量a 原子操作
a++; //2 拿a 进行a+1 赋值a 非原子操作
int b = a; //3 拿a b=a 非原子操作
a = a+1; //4 拿a 进行a+1 赋值a 非原子操作
可见性
一个变量被多个线程访问,如果一个线程改变了这个变量,其他线程能立即看到此修改,称之为可见性
有序性
Java中的有序性分为两个方面
线程内部观察:所有操作时有序的,其中一个线程的所有操作都是有序的“串行”
(as-if seial 像排序一样)
线程间观察: 在某一个线程中观察另一个线程,所有线程都是并行执行
交叉执行。
Volatile关键字
多线程情况下 : 当变量加上volatile关键字 后,变量对所有线程可见
特征:
- 保证内存可见性
- 禁止指令重排序
‘那么 为什么不加volatile的变量其他线程不可见?
这就要说到Java内存模型(JMM) 请看下图:
JMM-Java内存模型
java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信。java中的共享变量是存储在内存中的,多个线程由其工作内存,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。
在Java内存模型上,Java堆内存主要保存对象和基本数据类型的备份,称为主内存,Java栈中保存的变量的部份内存,称为本地内存(工作内存)。
而在下图可知:
- 每个线程再对变量的任何操作都是再自己的本地内存中进行,它是不会直接操作到主内存中的变量。
- 不同的线程是无法访问或者获取不同线程的本地内存的变量。
- 而线程之间变量的传递主要是通过主内存来完成,
而主内存和本地内存之间的交互是遵循具体的交互协议,从工作内存到主内存 主内存到工作内存
交互协议主要有8种:Lock 锁,unlock 解锁,read 读取,load 载入,use 使用,assign 赋值,store 存储,write 写入。
这些操作遵循原子性
这就是不加volatile其他线程不可见的原因。
看一个例子:
此例子中是要计算一秒中count++的次数
class volatileText{
private static boolean flag = true; //设置一个标志位
public static void main(String[] args) {
//线程A 进行count的计数
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (flag){
count++;
}
System.out.println("Count:" +count);
}
});
//线程B进行休眠1秒 然后设置标志位flag为false。计算1秒会计数多少次
Thread B = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);//休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false; //将标志位置为false
System.out.println("1min后结束");
}
});
A.start();
B.start();
}
}
首先来说结果,在flag没有加Volatile发现程序是无法结束的,这是不确定是否执行结束的
原因就是A线程无法获取flag变量的改变。
进行分析一哈:首先flag = true 变量在主内存中,线程A要获取 flag ,通过交互操作获取的是在本地内存A flag 的副本,而线程B在睡眠一秒后,开始获取 flag 的值 进行修改,而B线程也是从它相应的 本地内存B中 获取 flag 进行修改 false。
原因就在这
: 线程B 修改的是本地内存B 中的flag 副本,并没有修改主内存中的flag,而 线程B没有及时的写入到主内存中或者主内存没有及时的读取本地内存B 中的 flag, 然而 线程A或者 主内存没有主动交互跟新flag的值,所以 线程A没有获取到最新的值继而一直执行下去。
而加上 volatile
后
private static volatile boolean flag = true; //设置一个标志位
可以看到线程执行结束。
底层原理:
在汇编层面(非字节码 产生的.class)文件 ,可以看到在对应的汇编语句前加入了“Lock”(相当于内存屏障(栅栏)),当B线程修改flag变量操作时:
- 本地内存B将flag副本修改位最新值,并立即将最新值写到主内存中,通过总线将A线程的flag副本的标志(这里的标识是在底层每个变量都会有一个标识)置为无效。
- 当线程A访问flag的副本时,会先检查副本的标志位,若为无效,则不会读取副本中的值,而是会将主内存的最新值拷贝到本地内存副本中。
volatile工作原理
加了volatile的变量,在底层汇编层面 代码会多一个Lock前缀指令
,相当一个内存屏障(栅栏)
而它的作用:
- 它保证在重排序的时候不会将其后面的指令排到内存屏障前的位置,也不会将内存屏障前的指令排到内存指令之后
- 它会强制将工作内存中的数据立即写回到主内存。
- 如果是 写操作,它会立即导致其他CPU的对应的工作内存(缓存)立即无效
看到这后再来说说volatile的特征:
1.保证内存可见性
volatile修饰的变量副本(在本地内存中存储变量副本:Java虚拟机栈/寄存器(只能存储很少的变量,目的还是提高效率)) 它不会缓存到寄存器中,加了volatile后,只会储存到本地内存(虚拟机栈线程私有的空间),一旦变量修改就会立即写回到主内存,每一个线程访问主内存上数据都是最新的变量。如果此个变量是多个线程共享的变量,那么其他线程的本地副本变量的标志位会置为无效,从而其他线程会进入主内存重新获取
最新的变量值。
2.禁止指令重排序
Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序。
这里要清楚一件事:我们平时写的源码都会保存到 .java文件中 操作系统进行执行时会 转换为 .class 文件,而.class 文件还会再汇编层次再进行优化(全部转为二进制),目的还是为了提高效率。而汇编层次的优化请看下面的例子:
a = 10;
a++; 在汇编层面优化: a = 11;
b = 12; b = 12;
从结果来看 三行代码 优化为 两行 提高了效率。 从而可以看到 在优化前后数据最终结果保持不变的前提下,底层操纵系统会进行优化。但是在多线程的情况下
,不能保证代码的排序,按单线程情况下排序是没有问题的,但是在多线程情况下就会出现两个线程所看到的结果不一致的问题,所以会加volatile后 禁止指令的重排序。
而重排序又遵循happen-before
原则,它是操作系统设计之初就制定的规则。
happen-before
happen-before是JMM最核心的概念
Java内存模型(JMM)可以通过happen-before原则在多线程条件下保证了内存的可见性和操作的可见性。
具体的规则:
(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
参考为:https://blog.csdn.net/ma_chen_qq/article/details/82990603
注意:
volatile满足原子性吗:
volatile满足可见性,不保证原子性,也不能保证线程安全。有序性存在争议。
底层来说 它是保证有序性的,但是往大来说它又不满足有序性。
volatile修饰对象是否起作用:
volatile只能修饰变量
,对基本数据类型
起作用。
volatile修饰对象不起作用,只能保证对对象的地址空间进行可见,如果地址空间发生改变,其他线程能立即可见,但是如果对象本身的属性发生改变,volatile不能保证其他线程能立即可见。
主内存和工作内存是如何交互的?
如果volatile当前修饰的是一个变量
- 变量值从主内存(堆中)加载load到本地内存(虚拟机栈的栈帧中)。
- 线程对该变量的操作就在工作内存中,作用在副本中,在这之后如果主内存或者副本的数据发生改变都不互相联系,这就导致线程不安全,存在数据不一致的情况。
- 如果volatile修饰的变量在某线程中发生改变,会立即将该变量的修改写入到主内存中,其他线程的副本会立即失效,然后会重新拷贝主内存的最新值。·