先说结论
可以刷内存可见性的根本的方式有:volatile、 synchronized
延伸出来的其他方式有:AtomicInteger(使用了volatile)、System.out.println(使用了synchronized)、lock() 底层是volatile实现
建议:使用volatile和AtomicInteger无锁式代码效率高出同步器及锁很多,使用AtomicInteger最安全
更新内容:
happens before规则具体理解:
——如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1 写入v及之前的写操作都对线程2可见。也就是说,如果你感知到了volatile变量v的变化,那么在v之前的所有写操作你都可以感知的到,就算写的是非volatile变量。
——AQS 中的 state 是 volatile的. volatile为了保证可见性, 会在机器指令中加入lock指令, lock强制把缓存(工作内存)写回内存(主内存), 并失效其它线程的缓存行(MESI). 这里要注意的是, lock并不仅仅只把被volatile修饰的变量写回主内存, 而是把工作内存中的变更都写入主内存~
所以如果有任何一个volatile变量的修改或者是上锁、Atomic操作,都会将所有变量刷一遍,不止是volatile变量
测试内容
分两个线程分别只输出奇偶数并自增,到一百万为止。
主要有4种方式,这里测试了三种,主要思路是让两个线程while循环加if判断,这里重点在于两个线程间的数据通信,也就是刷主存的问题,如果不通信,则有可能会两边都陷入while死循环
-
synchronized+notify+wait
获取和释放同步器的时候,都会刷主存,所以使用这个没有任何内存可见性问题
效率比较低,100w需要8-9s,优点是不容易出错
public class Test3 implements Runnable{
public static int i =0;
public static final int total =1000000;
public boolean flag =true;
public void count() throws InterruptedException {
Long time = System.currentTimeMillis();
//重点在这一块,当wait()释放同步器的时候,会刷主存,另一边获取到同步器的时候,也会失效本地缓存
synchronized (Test3.class) {
if (flag) {
while (i < total) {
if (i % 2 == 0) {
i++;
Test3.class.notify();
} else {
Test3.class.wait();
}
}
} else {
while (i < total) {
if (i % 2 == 1) {
i++;
Test3.class.notify();
} else {
Test3.class.wait();
}
}
}
}
System.out.println(System.currentTimeMillis() - time);
}
public Test3(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
count();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Test3 t1 = new Test3(true);
Test3 t2 = new Test3(false);
Thread aThread = new Thread(t1);
Thread bThread = new Thread(t2);
aThread.start();
bThread.start();
}
}
-
lock
获取和释放锁的时候,都会刷主存
效率还不错,非公平锁只需要600ms,公平锁效率比较差,需要8s
缺点是1、需要注意使用非公平锁,2由于是手动获取和释放锁,所以植入的位置要考虑好,一定要放在变量判断条件之前,比如下面这个写法就会陷入死循环,因为没有利用到锁的内存可见性
public void count() throws InterruptedException {
Long time = System.currentTimeMillis();
if (flag) {
while (i < total) {
if (i % 2 == 0) {
test4.lock();
i++;
test4.unlock();
}
}
} else {
while (i < total) {
if (i % 2 == 1) {
test4.lock();
i++;
test4.unlock();
}
}
}
System.out.println(System.currentTimeMillis() - time);
}
加入System.out.println之后,可以在卡住之后几百毫秒的时间重新继续运行下去,因为它也有刷主存的作用,但是这样效率变得极低,而且不知道为什么还会卡住循环个一小段时间才能继续下去。看上去是要多次执行才有效果
public void count() throws InterruptedException {
Long time = System.currentTimeMillis();
if (flag) {
while (i < total) {
if (i % 2 == 0) {
test4.lock();
i++;
test4.unlock();
}
System.out.println(i);
}
} else {
while (i < total) {
if (i % 2 == 1) {
test4.lock();
i++;
test4.unlock();
}
System.out.println(i);
}
}
System.out.println(System.currentTimeMillis() - time);
}
正确的做法是这样,在判断前获取锁刷一下就可以了
public void count() throws InterruptedException {
Long time = System.currentTimeMillis();
if (flag) {
while (i < total) {
test4.lock();
if (i % 2 == 0) {
i++;
test4.unlock();
}else{
test4.unlock();
}
}
} else {
while (i < total) {
test4.lock();
if (i % 2 == 1) {
i++;
test4.unlock();
}else{
test4.unlock();
}
}
}
System.out.println(System.currentTimeMillis() - time);
}
更好的做法是将参数i设置成volatile类型,或者设置成AtomicInteger类型,后者还能保证原子性
public static volatile int i =0;
public static AtomicInteger i = new AtomicInteger(0);
效率也非常高,仅需40ms
最后说说System.out.println以及idea-DEBUG模式对内存可见性的影响
System.out.println方法中有同步synchronized的逻辑,会首先从主存获取数据,最后又会将数据刷到主存中,在多线程环境下会对程序运行有很大影响。而且效率也会极大降低,尽量不要使用
idea的DEBUG模式,经常会有一些迷幻效果,不断点和断点的效果不一致,这个最根本的原因就在于断点之后,会主动去刷一遍主存,这个时候你断点获得的数据和运行时的数据不一定是一致的,所以也有可能有的死锁状态,一个断点就解开了
一般debug或者日志遇到诡异的情况,可以先考虑这两点