一、线程安全要考虑三个方面:可见性、有序性、原子性
①可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果
②有序性指,一个线程内代码按编写顺序执行
③原子性指,一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队
二、volatile 能够保证共享变量可见性与有序性,但并不能保证原子性
①原子性举例
下面代码有一个共享变量balance=10,有两个线程一个-5,一个+5,后台打印出来是10。但如果运行上万次,就有可能不是这个结果。这样的代码虽然只有一句,并不是原子性的操作,我们从控制台打开,运行javap -p -v AddAndSubtract.class,查看他的字节码,发现相加方法或者相减方法的代码是多个字节码指令组成的。
public class AddAndSubtract {
static volatile int balance = 10;
public static void subtract() {
int b = balance;
b -= 5;
balance = b;
}
public static void add() {
int b = balance;
b += 5;
balance = b;
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
subtract();
latch.countDown();
}).start();
new Thread(() -> {
add();
latch.countDown();
}).start();
latch.await();
LoggerUtils.get().debug("{}", balance);
}
}
如下,cpu是在线程中高速切换的,如果发生以下交叉,输入的值是一个错误的结果
/**
t1 10
0: getstatic 读取静态变量
t2
0: getstatic 10 读取静态变量
3: iconst_5 准备数字5
4: isub 相减
5: putstatic 设置静态变量
5
3: iconst_5 准备数字5
4: iadd 相加
5: putstatic 设置静态变量
15
*/
②可见性举例
以下代码foo方法检测stop的值,如果一直为假则i不断相加,设置一个线程,在程序100ms后把stop设置为true,看是否能够成功停止。
public class ForeverLoop {
static volatile boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
get().debug("modify stop to true...");
}).start();
foo();
}
static void foo() {
int i = 0;
while (!stop) {
i++;
}
get().debug("stopped... c:{}", i);
}
}
从下面运行图得知,stop已经设置,但程序并没有停止。
我们先来看看网上的一个分析,这个说法是错误,这个解释是说变量的值并没有同步到内存中,那我们在写一个线程,在程序运行的0.2s后读取stop的值。
我们发现线程1是读取到了stop修改后的值,说明还是已经存入到内存,不然它是不会读取到的,他上图的说法不攻自破了。我们来详细解释一下这个可见性问题
所有的程度都交给cpu执行,cpu去查看线程1代码里的stop值是什么,根据他的值决定下一步操作,他到物理内存中读到stop的值,第一次他读到的是false,他不断的高速循环,经过测试,0.1s这个读取次数到达上万次,这么多次到物理内存都是false,内存的读写效率是比较低的,这时候JVM里面java即时编译器JIT,他负责代码的优化,任何一条java代码都会翻译成字节码指令,但是他还不能直接交给cpu执行,他还有一个解释器组件,他会将字节码逐行翻译成机器码,再交给cpu执行。JIT来对一些热点的字节码进行优化,反复进行的代码就是热点代码,这个循环次数超出他的优化阈值,他对这个while循环进行了一个很大的优化,直接将stop替换成了false。那问题就来了,线程2改了stop的值,线程1完全不知情,因为机器码已经被替换了。
③有序性举例
通过第三方插件进行数亿次的压力测试,来体现这个效果。actor1是线程1给xy复制,而actor2是线程2读取xy值。他的值可能有很多种组合,看控制台输出结果。
volatile不同位置会影响压力测试结果,volatile相当于一个内存屏障,让他上面的代码无法排到他下面去,读取x的屏障也不能越到读取上去y