线程安全性问题可以总结为:可见性、原子性、有序性这几个问题,接下来我们通过代码来复现可见性、原子性的问题,后面章节我会从原理层面去分析这些问题。
可见性
public class VisibleDemo {private static boolean stop = false;public static void main(String[] args) {Thread thread = new Thread(new Runner());thread.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}stop = true;}private static class Runner implements Runnable {private int i;@Overridepublic void run() {while (!stop) {i++;}System.out.println(i);}}}
在 main 方法中把 stop 变量改为 true,理论上线程应该终止,但是一般情况下,子线程一直在运行没有终止。说明在 main 线程中改变 stop 的值没有被子线程获取到,这就是多线程环境下的可见性问题。
原子性
public class AtomicDemo {private static int count;public static void main(String[] args) {for (int i = 0; i < 1000; i++) {new Thread(new Runner()).start();}try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("运行结果:" + count);}private static class Runner implements Runnable {@Overridepublic void run() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count++;}}}
在 main 方法中,循环创建1000个线程执行 count++,理论上 count 最终的运行结果应该是1000,但是我们发现运行结果会出现小于等于1000的情况,这就是多线程环境下的原子性问题。
运行结果:973
我们可能存在如下疑问:
- 在属性 count 前面加上 volatile 关键字能否解决原子性问题?
- 为什么 count++; 会存在原子性问题呢,它不是只有一条命令么?
注意:count++; 不是原子操作,通过 javap **命令反汇编查看如下代码的指令信息。**
public class Test {private int count = 0;public void incr() {count++;}public static void main(String[] args) {new Test().incr();}}
public class test3.Test {public test3.Test();Code:0: aload_01: invokespecial #10 // Method java/lang/Object."<init>":()V4: aload_05: iconst_06: putfield #12 // Field count:I9: returnpublic void incr();Code:0: aload_01: dup2: getfield #12 // Field count:I5: iconst_16: iadd7: putfield #12 // Field count:I10: returnpublic static void main(java.lang.String[]);Code:0: new #1 // class test3/Test3: dup4: invokespecial #21 // Method "<init>":()V7: invokevirtual #22 // Method incr:()V10: return}
通过上面的指令信息发现,count++ 其实是由三条指令组合的复合操作。
getfieldiaddputfield
volatile 关键字不能解决复合操作的原子性问题,所以在属性 count 前面增加 volatile 关键字不能解决原子性问题。
有序性
指的是在程序运行过程中,代码的执行顺序和我们编写代码的顺序可能是不一致的,因为在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/ygfq5z 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
