volatile详解跟伪共享问题
并发编程的三个核心特性
原子性:一个或多个操作在CPU中执行时,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行
可见性:当多个线程访问同一个变量时,一个线程改变了这个变量的值,其他线程能够立即看到修改的值
有序性:程序执行的顺序按照代码的先后顺序执行。JVM存在指令重排,所以存在有序性问题
volatile详解
保障可见性跟有序性,不保障原子性。使用volatile修饰的字段
demo
线程B休眠1秒,用于保障线程A先执行。线程A读到flg的变化后退出循环
public class volatileDemo {
private static boolean flg = false;
public static void main(String[] args) {
//线程A
new Thread(() -> {
while (true) {
if (flg) {
System.out.println("线程A读到了修改 结束");
break;
}
}
}).start();
//线程B
new Thread(() -> {
try {
Thread.sleep(1000); // 等待 1 秒钟,确保线程A先运行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flg = true;
}).start();
}
}

运行后可以发现,项目不会结束 线程A陷入死循环,读不到线程B的修改。这个时候需要使用volatile保障线程之前的可见性
伪共享
一致性协议
人们设计了各种模型和协议,如MSI、MESI、write-once等等协议,我们现在用的比较多的是MESI协议。简单理解就是内存中有一块共享的主存,修改后主存把修改广播到各个线程缓存块之间
demo
尝试多次运行 volatile跟不加的运行结果 比较后 加上关键字跟不加速度相差50倍
class Pointer {
volatile long x;
volatile long y;
}
public class CacheSharedTest {
public static void main(String[] args) throws InterruptedException {
testPointer(new Pointer());
}
private static void testPointer(Pointer pointer) throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(pointer.x+","+pointer.y);
System.out.println(System.currentTimeMillis() - start);
}
}
class Pointer {
long x;
long y;
}
造成这个现象的原因是:以MESI内存一致性协议为例,行是内存的基本单位,一个内存行被分为了64个字节,我们知道一个long类型占用8个字节。所以一个long类型x不能占满一行,他会跟y共用一个内存行。线程1中更改了x的变量后,变量x对应的缓存行就会失效,线程2就不能直接从缓存行中 来直接读取y,需要再次从主存加载进缓存,y的更改也会影响x的使用,就造成了变量x和y相互影响的情况,就导致了异常耗时。
解决方式
@Contended可以作用于类上,也可以作用于变量上。它也是通过填充缓存行来避免伪共享
占满一个内存行