概述
前文提到了并发的三大特性,其中包含可见性,有序性和原子性。volidate和synchronized分别满足了不同特性从而在并发编程中发挥了一定的角色。
本文概述
1.1 volidate的概述
volidate保证了三大特性中的可见性,即一个线程修改使用了volidate的变量,修改后的值会被其它线程所读取。
1.2 不可见性产生的原因
产生的根本原因是cpu的内存模型,每个线程都工作在自己的工作内存中,只有当工作内存失效时才会到主内存中读取数据。(详情参考:并发的三大特性)
1.3 volidate防止指令重排序
volidate采用了内存屏障的方式,保证了可见性,也就是在读取或写入volidate修饰的变量之前,插入读屏障或写屏障。
volatile的内存屏障策略:
- 会在volatile读:前面加入LoadLoad屏障、后面LoadStore屏障;
- 在volatile写:前面加入StoreStore屏障、后面加入StoreLoad屏障。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
1.4 volidate保证内存可见性
保证内存可见性是通过原子类指令执行。 常见的指令: lock unlock read:从主内存加载到工作内存 load:从工作内存加载到工作内存的变量副本 use:工作内存的变量副本->执行引擎 assign:执行引擎->工作内存的变量副本 store:工作内存的变量副本->工作内存 write:工作内存->主内存
使用了volidate关键字修饰的变量,读取时read、load、use按序执行
写入时assign、store、write按序执行
由此保证了内存可见性
1.5 volidate常见面试题
-
保证内存可见性的方法、原理 内存可见性产生的原因是每个线程都工作在自己的工作内存中。
-
工作内存在何时会失效?探讨不使用volidate的情况下如何保证可见性 在三种情况下,工作内存会失效
- 线程中释放锁时(调用wait等)
- CPU空闲时(IO操作,System.out.xxx操作)
- 线程切换时(sleep切换)
2.1 synchronized概述
synchronized保证了可见性、有序性和原子性。 锁一定锁的是对象,对于静态方法,锁的是类的.class对象,对于普通方法锁定的是this,对于代码块,需明确指定锁定的类型。
对于方法的锁定,是利用ACC_SYNCHRONIZED锁定 对于代码块的锁定,是利用monitorentry和monitorexit进行锁定
同时,对于代码块的锁定,会有两个monitorexit,其中一个是自动生成的异常处理时使用的monitorexit。
2.2 synchronized面试题
synchronized如何保证可见性、有序性和原子性? 这个问题的讨论基本可以涵盖关于synchronized的所有面试题
原子性的保证:利用锁机制保证只有一个线程可以执行代码块或方法,但是要注意使用时对于其它静态同步方法的阻碍。 有序性:利用as-if-serial机制,同时保证只有单个线程会执行,从而保证了有序性(但不能防止指令重排序) 可见性:在解锁前会将变量的最新值刷回主内存,从而保证了可见性