【并发编程】
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程 t 不会如预想的停下来
}
为什么无法退出该循环?
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决办法
使用
volatile
(易变关键字)它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
[](()2-2 可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
注意:
synchronized
语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。
但缺点是
synchronized
是属于重量级操作,性能相对更低。
如果在前面示例的死循环中加入
System.out.println()
会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
进入`println`源码,可以看出加了`synchronized`,保证了每次`run`变量都会从主存中获取
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
[](()3.有序性
[](()3-1 诡异的结果
看下面一个栗子:
int num = 0;
boolean ready = false;
// 线程 1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程 2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
看到这里可能聪明的小伙伴会想到有下面三种情况:
情况 1:线程 1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况 2:线程 2 先执行 num = 2,但没来得及执行 ready = true,线程 1 执行,还是进入 else 分支,结果为 1
情况 3:线程 2 执行到 ready = true,线程 1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
但其实还有可能为 0 哦! ??
有可能还是:线程 2 执行 ready=true ,切换到线程 1 ,进入 if 分支,相加为 0,在切回线程 2 执行 num=2
这种现象就是指令重排
[](()3-2 解决方法
volatile
修饰的变量,可以禁用指令重排
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;//可以禁用指令重排
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】}
}
[](()3-3 有序性理解
同一线程内,JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,看看下面的代码:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时, 既可以是
i = ...; // 较为耗时的操作
j = ...;
也可以是
j = ...;
i = ...; // 较为耗时的操作
这种特性称之为指令重排,多线程下指令重排会影响正确性
[](()3-4 happens-before
happens-before 规定了对共享变量写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start()
线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用
t1.isAlive()
或t1.join()
等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通 过
t2.interrupted
或t2.isInterrupted
)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);//10
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);//10
}
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
具有传递性,如果
x hb-> y
并且y hb-> z
那么有x hb-> z
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;//写屏障,y 也会同步到主存
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();
以上变量都是指共享变量即成员变量或静态资源变量
评论