线程间操作的定义
- 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。
- Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。
线程间操作有:
- read操作(一般读,即 非volatile读)
- write操作(一般写,即 非volatile写)
- volatile read
- volatile write
- Lock.(锁monitor)、Unlock
- 线程的第一个和最后一个操作
- 外部操作
所有线程间操作,都存在可见性问题,JMM需要对其进行规范
volatile关键字
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。 Java内存模型规定:对volatile变量v的写入,与所有其他线程后续对v的读同步。
要满足这些条件,所以volatile关键字就有这些功能:
- 禁止缓存:
volatile变量的访问控制符会加个
ACC_VOLATILE
——https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
- 对volatile变量相关的指令不做重排序。
double和long的特殊处理
由于《Java语言规范》的原因,对非volatile的double、long的单次写操作是分两次来进行的,每次操作其中32位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。
- 读写volatile修饰的long、double是原子性的。
- 商业JVM不会存在这个问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。
- 《Java语言规范》中说道: 建议程序员将共享的64位值(long、double)用volatile修饰或正确同步其程序以避免可能的复杂情况。
对于同步的规则定义
-
对volatile变量v的写入,与所有其他线程后续对v的读同步。
-
对于监视器m的解锁与所有后续操作对于m的加锁同步。
-
对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同步。
-
启动线程的操作与线程中的第一个操作同步。
-
线程T2的最后操作与线程T1 发现线程T2已经结束同步。(isAlive,join可以判断线程是否终结)
-
如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步。通过抛出
InterruptedException
异常,或者调用Thread.interrupted
或Thread.isInterrupted
Happens-before现行发生原则
happens-before 关系用于描述两个有冲突的动作之间的顺序,如果一个action happens before 另一个action,则第一个操作被第二个操作可见,JVM需要实现如下happens-before规则:
- 某个线程中的每个动作都happens-before 该线程中该动作后面的动作。
- 某个管程上的unlock 动作 happens-before 同一个管程上后续的 lock 动作。
- 对某个 volatile 字段的写操作 happens-before 每个后续对该volatile字段的读操作。
- 在某个线程对象上调用
start()
方法 happens-before被启动线程中的任意动作。 - 如果在线程t1中成功执行了
t2.join()
,则t2中的所有操作对t1可见。 - 如果某个动作 A happens-before 动作 B,且B happens-before 动作C,则有 A happens-before C。
当程序中包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据竞争。遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!