这个程序运行结果会是什么?
public class Main {static class ListAdd { private static List list = new ArrayList(); public void add() { list.add("baoer"); } public int size() { return list.size(); } } public static void main(String[] args) throws IOException { final ListAdd list = new ListAdd(); new Thread(new Runnable() { @Override public void run() { try { for (int i=0 ; i < 10; i++) { Thread.sleep(500); list.add(); System.out.println("线程:" + Thread.currentThread().getName() + " 添加了一个元素" ); } } catch (InterruptedException e) { e.printStackTrace(); } } },"T1").start(); new Thread(new Runnable() { @Override public void run() { while (true) { if (list.size()== 5) { System.out.println("当前线程收到通知 " + Thread.currentThread().getName() + " list.size=5线程停止"); throw new RuntimeException(); } } } }, "T2").start(); // fun(); }
如果知道ArrayList不是线程安全的也许答案就是线程T1运行结束,T2一直执行下去不会抛出异常而结束。事实结果也确是这样。但这样的执行结果背后却值得深思。
问题1:这是因为有可能T2线程某次读入缓存的size为4,但下一次读入缓存的数字是6,所以永远进入不了if.
但由于线程T1每次add之后都sleep 500 毫秒所以这种可能不存在。
问题2:在T2 if(list.size() == 5) 之前将size放入一个Hashset发现Hashset中只有一个值 0 。这说明size根本没有从主内存中刷新到T2工作内存中,为什么主存中size值都更新了还不刷新到工作内存中呢?不是有缓存一致性吗?
原因就是 T2中的size根本没有在T2的缓存中!这是编译器干的事! 编译器发现是一个while(true),并且要频繁使用size,就会把size放在寄存器中提高访问速度,缓存不保存size。所以即使有缓存一致性size永远无法更新。
问题3: 为什么list是volatile 就会得到正确结果?
对于volatile变量 编译器不会把它放入寄存器中,在缓存中volatile 可以保证可见性,并且根据happens-before规则 volatile 的读取一定在写入之后。
问题4: 为什么在if判断前加 System.out.println(list.size()); 也会的到正确结果?
通过查看System.out.println 源码发现执行输出语句时要加同步锁,
JMM关于synchronized的两条规定:
1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
2. 线程加锁时,先清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
由于清空工作内存中的值所以寄存器中的值也失效了,虽然此时值没在工作内存中,但也寄存器也会刷新再从工作内存中读取。
问题5: 为什么在if判断前加Thread.sleep(0)或者Thread.yield();也会的到正确结果?
这涉及到了线程的上下文切换,一但切换上下文工作内存中的就值就会失效,系统保存了线程的状态,下次切换回来时重新从内存中读值。