一、先看一个程序
public class VisibilityDemo {
static boolean check = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(!check) {
// System.out.println("你咋不结束");
}
}).start();
Thread.sleep(100);
check=true;
}
}
执行后发现,明明check改为true了,但是子线程依然在执行死循环。
二、问题产生的原因
每个线程都存有一份数据副本,可以理解为一份只有当前线程可读的缓存。如图
变量check本来存在内存中中,线程A读取了并缓存。
线程B也读取了并缓存。此时线程A把check修改成了true,但线程B依然只读取自己缓存的check,导致变量不可见。
有细心的朋友发现了,把 System.out.println(“你咋不结束”); 注释去掉程序就不会进入死循环了,这是因为有些语句会触发线程重新从内存中读取变量。
三、解决的办法
添加volatile关键字
public class VisibilityDemo {
volatile static boolean check = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(!check) {
// System.out.println("你咋不结束");
}
}).start();
Thread.sleep(100);
check=true;
}
}
四、volatile作用
// TODO
五、三级缓存
cpu缓存实际的模型:
L1,L2,L3,执行速度逐渐下降,空间逐渐变大,成本逐渐降低
六、缓存行概念
线程读取L3时并非是一个个变量的读的,而是以一块一块的读的,这一块数据被称为缓存行,一般是64个字节(因特尔CPU,啥你说为啥是64字节,因为少了就快又命中率低,多了就慢又命中率高,64是一个折中值)。
下面我们看一个程序:
public static class T {
// protected long p1,p2,p3,p4,p5,p6,p7;
public volatile long x = 0L;
// protected long p8,p9,p10,p11,p12,p13,p14;
}
public static void main(String[] args) {
int count = 1_0000_0000;
Long sysTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(2);
T[] ts = new T[2];
ts[0] = new T();
ts[1] = new T();
Thread t1 = new Thread(()->{
for(int i=0; i<count; i++) {
ts[0].x = 1L;
}
latch.countDown();
});
Thread t2 = new Thread(()->{
for(int i=0; i<count; i++) {
ts[1].x = 1L;
}
latch.countDown();
});
t1.start();
t2.start();
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("耗时:"+(System.currentTimeMillis()-sysTime));
}
程序功能是2个线程修改T数组里的变量x。
执行后耗时约2000ms。然后把2个protected注释去掉,执行耗时变成了800ms。
PS:笔者用的是macbook M1处理器,盲猜缓存行是128字节的,加到p28才演示出效果。
-
为何会产生这样的效果?
这是因为2个T对象的x原本存在同一个缓存行。线程A修改了T1的x,线程B修改T2时,跟T1说我改了这行,你也要再读一下。这种现象也称为伪共享。 -
为何去掉注释变快了?
因为每个long占8个字节,x前后都塞了54个字节,2个x在同一缓存行,杜绝伪共享的发生。
-
太难看了,有无更好的写法?
有,就是@Contended注解,用法如图:
@Contended
public volatile long x = 0L;
注:此注解只在jdk1.8,并且要添加启动参数:
-XX:-RestrictContended
才能生效