学到了什么?
线程与锁模型的三个主要危害:
- 竞态条件:即代码行为取决于个操作的时序;
- 死锁:当需要持有多把锁时,如果获取锁的顺序不一样,则有可能死锁;
- 内存可见性:编译器、JVM以及硬件都有可能进行重排序,导致线程看到错误的值。同时,缓存的存在可能导致线程看不到另一个线程的修改。
避免危害的准则:
- 对共享变量的所有访问都进行同步;
- 读线程和写线程都进行同步;
- 按照约定的全局顺序来获取多把锁;
- 当持有锁时避免调用外星方法(就是自己无法控制的方法);
- 持有锁的时间应该尽可能的短。
自习
- 阅读William Pugh的网站:Java内存模型;
这个网站像是一个目录,列举了各种与Java内存模型有关的资料地址。 - 自学JSR 133(Java内存模型)的FAQ;
学习成果:https://www.jianshu.com/p/145e8b98c509 - Java内存模型是如何保证对象初始化是线程安全的?是否必须通过枷锁才能在线程之间安全的公开对象?
对象初始化的线程安全问题其实就是由于对象还未初始化完就赋值给某个全局引用,然后被其他线程操作。但只要属性都是final的,JMM就保证在将对象赋值给某个引用前,final属性一定会被初始化。但如果不全是final属性,那就需要通过volatile来解决了,但加了volatile后,变量的读取性能会下降。
是否一定需要加锁才能在线程之间安全的公开对象,按我的理解,如果对象是真正的不可变的,那就不需要。 - 了解反模式“双重检查锁模式”(double-checked locking)以及为什么称其为反模式。
双重检查锁模式,顾名思义,就是进行了两次检查,并加锁,而且是加在第二次检查的时候(这个顾名思义不出来)。它是来实现一个能延迟初始化的线程安全的单例模式。双重检查就是进行了两次非空检查,锁就是在第二次检查的时候枷锁。
之所以被称为反模式就是因为上面讨论的,对象的初始化不一定是线程安全的。在单例对象还未初始化完成的时候,就被赋值给了全局的单例引用,另一个线程获取单例的时候,由于第一次检查没有加锁,所以就可以得到这个还未完全初始化的对象。
实践
- 对于哲学家进餐问题,用最开始有死锁隐患的代码做一些实验。尝试修改哲学家思考状态的时长、进餐状态的时长以及进餐的人数。这些变量对于出现死锁的时机有什么影响?设想我们正在进行调试,那应该如何增大出现死锁的几率?
运行了三次才出现死锁(半小时、一小时、五小时)。将人数调整为三人后,运行了两次出现死锁(一小时、两个半小时),调整为一人运行了一个小时即出现了死锁。将时间调成50毫秒,很快就出现了死锁。由此可见,人数越少,时间越短越容易死锁。其实想想也是,死锁本质可以看成一个概率问题,人数越少意味着死锁的环越小,也就是死锁的概率大,时间越短则意味着频率增大,这样概率大,频率大,复现的时间当然短。所以,当我们在调试时,也可以从增加缩小死锁的环,以及增加运行频率这两方面入手。 - (困难)编写一段程序,在不使用同步的前提下,模拟内存写操作的乱序执行。这个任务之所以有难度,是因为Java内存模型可能不会优化过于简单的例子,故找到这个优化场景比较困难。
网友评论