深入Volatile
深入Volatile
1、变量不可见性:
1.1多线程下变量的不可见性
直接上代码
/**
* @author yourkin666
* @date 2024/08/12/16:12
* @description
*/
public class h1 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.start();
while (true) {
if (myClass.isFlag()) {
System.out.println("circle!!!");
}
}
}
}
class MyClass extends Thread{
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag = true;
System.out.println("flag = "+ flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
子线程从主内存中读取数据放到工作内存,将flag修改为true,但是此时flag的值还没有写回主内存,所以主线程读取的flag依然是false
当子线程将flag值写回主内存后,main函数里面的while(true)调用的是系统比较底层的代码,速度较快,没时间再去主存中读取flag
所以while(true)读取到的值一直是false(当然主线程可能在一个时刻去主内存读取最新的值,我们无法控制)
1.2变量不可见性内存语义
JMM(java内存模型)是针对于Java并发编程的模型
JMM规范:
- 所有共享变量(实例变量和类变量)都存在于主内存,不包含局部变量,因为局部变量是线程私有
- 每一个线程都还有自己的工作内存,在工作内存中,保存了一份共享变量的副本
- 线程对变量的所有操作都在工作内存中完成
- 不同线程的工作内存是隔离的
可见性问题的原因:
- 所有共享变量都存在于主内存,每个线程都有自己的工作内存,而且线程读写共享数据也是通过本地内存交换的,所以导致了可见性问题
1.3变量不可见行的解决方案
第一种是加锁
第二种是使用volatile关键字
加锁:
while (true) {
synchronized (myClass) {
if (myClass.isFlag()) {
System.out.println("circle!!!");
}
}
}
线程进入synchronized代码块前后的执行过程:
- 线程获得锁
- 清空工作内存
- 从主内存拷贝共享变量的最新值
- 执行代码块
- 将修改后的副本的值刷新回主内存中
- 线程释放锁
volatile关键字:
private volatile boolean flag = false;
工作原理:
- 子线程从主内存读取数据放在工作内存,修改flag值后,还没有写回主内存
- 此时主线程读取到了flag值为false
- 当子线程将flag写回主线程后,就失效工作内存中的拷贝值
- 再次对flag进行操作时,线程会从主内存读取最新值,放入工作内存
volatile关键字保证不同线程对共享变量操作的可见性,也就是一个线程修改了volatile修饰的变量,当此变量写回主内存后,其他线程可以立即看到最新值
volatile修饰的变量可以在多线程并发修改下,实现线程间变量的可见性
2、volatile其他特性:
1.volatile不保证原子性
/**
* @author yourkin666
* @date 2024/08/12/16:12
* @description
*/
public class h1 {
public static void main(String[] args) {
Runnable myClass = new MyClass();
for (int i = 1; i <= 100 ; i++) {
new Thread(myClass).start();
}
}
}
class MyClass implements Runnable{
private volatile int count = 0;
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
count++;
System.out.println("count >>>>" + count);
}
}
}
count++是非原子操作,起码可以被分为三步
在多线程环境下,volatile修饰的变量也是线程不安全的
要保证数据的安全性,还得是锁,或者原子类
原子类:
java.util.concurrent.atomic(Atomic包)
这个包中的原子操作类提供一种用法简单、性能高效、 线程安全地更新一个变量的方式
AtomicInteger:
原子型Integer ,可以实现原子更新操作:
public AtomicInter()
初始化一个默认值为0的原子型Integet
public AtomicInter(int initialValue)
初始化一个指定值的原子型Integet
int get()
获取值
int getAndIncrement()
以原子方式将当前值加1,返回自增前的值
int incrementAndget()
以原子方式将当前值加1,返回自增后的值
int addAndGet(int data)
以原子方式将输入的值与AtomicInteger里的value相加,并返回结果
int getAndSet(int value)
以原子方式设置newValue的值,并返回旧值
2.禁止指令重排序
为了提升程序的运行效率,编译器和处理器常常对指令进行重排序
使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题
3、volatile内存语义:
volatile写读建立的happens - before关系:
从JDK5开始,提出happens - before概念,来阐述操作之间的内存可见性。
如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在happens - before关系。(这里提到的两个操作既可以是在一个线程中,也可以是不同线程之间
happens - before规则:
-
单线程规则
同一个线程中,前面的所有写操作对后面的操作可见
-
锁规则(synchronized、Lock等)
如果线程1解锁了monitor A ,接着线程2锁定了A,那么,线程1解锁A之前的写操作都对线程2可见
-
volatile变量规则
如果线程1写入volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都是对线程2可见的
-
传递性
A对B可见,B对C可见,那么A对C可见
-
start()规则
假设线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A在线程B执行前对共享变量的修改,是对线程B可见(线程B启动后的,线程A对变量的修改,线程B就未必看得到了
-
join()规则
线程t1写入的所有变量,在任意其他线程t2调用t1.join(),或者t1.isAlive()成功返回后,都对t2可见