读懂Java并发系列——3.volidate、synchronized的语义和作用

436 阅读3分钟

概述

前文提到了并发的三大特性,其中包含可见性,有序性和原子性。volidate和synchronized分别满足了不同特性从而在并发编程中发挥了一定的角色。

本文概述

1.1 volidate的概述

volidate保证了三大特性中的可见性,即一个线程修改使用了volidate的变量,修改后的值会被其它线程所读取。

1.2 不可见性产生的原因

产生的根本原因是cpu的内存模型,每个线程都工作在自己的工作内存中,只有当工作内存失效时才会到主内存中读取数据。(详情参考:并发的三大特性image.png

1.3 volidate防止指令重排序

volidate采用了内存屏障的方式,保证了可见性,也就是在读取或写入volidate修饰的变量之前,插入读屏障或写屏障。

volatile的内存屏障策略:

  1. 会在volatile读:前面加入LoadLoad屏障、后面LoadStore屏障;
  2. 在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:工作内存->主内存

image.png 使用了volidate关键字修饰的变量,读取时read、load、use按序执行 写入时assign、store、write按序执行 由此保证了内存可见性

1.5 volidate常见面试题

  1. 保证内存可见性的方法、原理 内存可见性产生的原因是每个线程都工作在自己的工作内存中。

  2. 工作内存在何时会失效?探讨不使用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机制,同时保证只有单个线程会执行,从而保证了有序性(但不能防止指令重排序) 可见性:在解锁前会将变量的最新值刷回主内存,从而保证了可见性