一. volatile
1. volatile的语义
Java内存模型对volatile关键字定义了一些特殊规则. 首先从volatile的语义开始说起, 再得出Java内存模型对volatile设定的几个规则
-
语义
- 禁止volatile代码附近指令重排
- 何为指令重拍: 普通变量只能保证在依赖其他变量的结果进行计算时可以获得正确结果, 但不能保证变量的赋值顺序和代码中的顺序一致. 这也是满足了Java内存模型中"线程内部表现为串行"的语义.
- 指令重拍是机器级的优化, CPU往往会把赋值操作的语句和不依赖该变量的计算语句不按照代码顺序执行, 这是提高CPU执行效率的一种手段. 比如:
MEM阶段访问的数据不在cache中,需要从外部存储器获取,这个动作需要几十个cycle,如果顺序执行,后面的指令MEM都要等待这个指令操作完成。乱序执行是说,先执行后面不依赖该数据的指令 a = 0; b = 0; a = a + 1; b = b + 1 上面4行代码的执行顺序可能变为 a = 0; a = a + 1; b = 0; b = b + 1(避免寄存器对变量a和变量b之间反复切换, 增大内存取值花费的时钟周期)
- volatile如何避免指令重拍?
当变量被volatile修饰后, 转换后的汇编代码会在赋值语句后面加上一个内存屏障, 在CPU指令重拍时, 不能能把后面的指令重排序到内存屏障之前
- 保证volatile修饰的变量的可见性
- 前面提到, 被volatile修饰的变量, 转换成汇编代码后会在复制操作后加上内存屏障, 避免指令重拍. 该内存屏障还有一个作用是: 如果某个变量被多个CPU缓存在cache中, 当本CPU的cache写入内存时, 该写入动作也会导致其它CPU无效化其cache, 使得其它CPU再次使用到该变量时只能从内存中重新取值. 相当于其它CPU总能使用该变量的新值
- 禁止volatile代码附近指令重排
-
Java内存模型对volatile关键字的特殊规则
- 要求在工作内存中, 每次使用变量前都必须从主内存刷新最新的值, 保证能看见其他线程对该变量修改后的值
- 每次修改变量后, 都要立刻同步回主内存, 保证其它线程可以看见自己对该变量的修改
- 要求volatile修饰的变量不会被指令重拍优化, 保证执行顺序与代码的书写顺序一致
2. 对long和double类型变量的特殊规则
JVM允许将未被volatile修饰的64位数据类型的读写操作, 划分为两次32位的操作来执行. 如果多个线程共享一个未被声明成volatile的long或double类型的变量, 并同时对他进行读取和修改, 则可能会读到一个既非原值, 也不是其它线程修改后的值的"半个变量". 不过这种"读到半个变量"的情况已经十分罕见, 因为商用虚拟机几乎都会把64位数据的读写操作作为原子操作, 因此在编写代码时一般不需要把long和double变量专门声明为volatile
二. Synchronized
1. Synchronized使用方法
-
类级别的锁 (所有同步针对该类的所有对象)
- 同步静态方法
同步静态方法是类级别的锁,一旦任何一个线程进入这个方法,其他所有线程将无法访问这个类的任何同步类锁的方法。public synchronized static void fun() { }
- 同步代码块锁类
下面提供了两种同步类的方法,锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。private void fun() { synchronized (this.getClass()) { } }
- 同步静态方法
-
对象计别的锁 (所有同步只针对同一个对象)
- 同步普通方法
public synchronized void fun() { }
- 同步代码块中使用this对象/其它对象作为锁
public void fun() { synchronized (this) { } } public void fun() { synchronized (LOCK) { } }
- 同步普通方法
2. synchronized与wait,notify合用
单一的synchronized虽然可以保证线程安全, 但需要配合其它线程方法, 才能表示复杂逻辑的线程交互
-
obj.wait()
- 使用方法
synchronized(obj){ while(条件){ obj.wait(); // 收到通知后, 继续执行 } }
- 使用wait()之前, 需要获取对象锁.
- 其次, wait()方法要写在while循环中, 并指明跳出循环的条件 :
因为wait()别唤醒后, 原先的判断条件可能已经发生改变, 需要再次判断 - 最后, wait()方法执行时, 线程会释放得到的obj独占锁, 并进入'等待阻塞'状态, 等待其它线程执行该
obj
锁的notify()
-
obj.notify()
当等待在obj上的线程收到一个obj.notify()
时, 就能重新获得obj的锁.值得注意的是以下3点 :- 当线程执行完
obj.notify()
后, 不会立刻释放锁, 而是等待synchronized代码块中的代码全部执行完毕后再释放锁 - 如果有多个线程在方法
obj.wait()
中, 则只会随机选择一个线程唤醒 -
obj.notifyAll()
会唤醒所有在执行obj.wait()
的线程
- 当线程执行完
-
Thread.sleep
sleep方法会让线程休眠, 但不会释放已获得的锁
三. Java内存模型
1. 什么是Java内存模型
Java内存模型(Java Memory Model), 是用来屏蔽各种硬件和操作系统内存访问差异, 实现让Java程序在各种平台下都能达到一致的内存访问效果的模型; 其主要目标是"定义程序中变量的访问规则", 即2个内存访问细节:将变量存储到内存
和从内存中取出变量
-
这里的"变量"
此处的变量与Java程序中所说的变量有所不同, 它专指实例的字段
,静态字段
,数组中的元素
, 它不包括"局部变量"与"方法参数". 因为后者是线程私有的, 不会被共享也就不会存在竞争的问题 -
JMM没有做出的限制
为了获得较好的执行效能, Java内存模型没有限制:- 执行引擎使用cpu中特定寄存器或特定缓存来和主内存交互
- 也没有限制JIT不能调整代码执行顺序这类优化措施
-
什么是主内存, 什么是工作内存
Java内存模型规定所有变量都存储在主内存
中, 此外每个线程还有自己的工作内存.- 线程的工作内存中保存了被线程使用的变量的主内存副本拷贝;
- 线程对变量的所有操作都是在工作内存中执行的, 线程不能直接读写主内存的变量
- 线程间, 变量值得传递需要通过主内存来完成.
这里的主内存和工作内存是对物理内存,CPU cache, 寄存器的一种抽象, 有别于Java内存区域中的"堆","栈","方法区". 二者不是同一层次的内存划分, 基本没有关系. 如果要勉强对应起来, 那从变量, 主内存, 工作内存的定义来看:
* 主内存对应Java堆中对象实例的数据部分
(对象实例还包括hash码, GC标志, GC年龄, 同步锁等信息)
* 工作内存对应栈的部分区域
从更底层上说:
* 主内存对应物理内存
* 而工作内存往往对应于于寄存器和CPU高速缓存. 因为程序运行时往往访问的是工作内存的变量, 虚拟机会优先把这些变量拷贝到cache或寄存器中
2. 主内存与工作内存的互相操作
Jvm规定了8种操作, 用来实现主内存和工作内存之间相互拷贝的实现细节. 这8种操作都是原子的, 不可再分的. (对于long和double类型的变量可能有例外)
-
作用于主内存的操作
-
lock
: 将主内存中的变量标识为, "已被一条线程独占"的状态 -
unlock
: 将主内存中处于"lock"状态的变量释放出来, 释放以后该变量才能被其他线程"lock" -
read
: 将变量的值, 从主内存传输到工作内存中, 以便后续执行load
动作 -
load
: 把从主内存拷贝过来的变量值, 赋给工作内存的变量副本
-
-
作用于工作内存的操作
-
use
: 把工作内存中, 某个变量的值传递给执行引擎. 每当jvm遇到一个需要用到变量值的字节码指令时就会去执行该动作 -
assign
: 把从执行引擎收到的值, 赋给工作内存中的某个变量. 每当jvm遇到一个给变量赋值的字节码指令时就会去执行该动作 -
store
: 将工作内存中某个变量的值传递到主内存, 以便后续执行write
动作 -
write
: 把从工作内存传递过来的某个值赋给主内存的某个变量
-
因此Java内存模型的主要规则是:
如果要把主内存中某个变量的值拷贝到工作内存, 则顺序执行`read`和`load`动作;
如果要把工作内存中某个变量副本的值写回到主内存, 则顺序执行`store`和`write`动作
Java内存模型, 只要求以上2个操作是顺序执行的, 而不保证是连续执行的. 也就是说. read
和load
之间, store
和write
之间可以插入其他指令. 一种多线程下可能会导致歧义的顺序是:
read a, read b, load b, load a
- Jvm还规定了, 以上8种操作必须必须满足如下规则
-
read
和load
, 以及store
和write
, 必须成对出现.
即不允许某个变量从主内存读取了但工作内存不接受, 或者从主内存发起了写回但主内存不接受写回的情况 - 不允许线程丢弃某个变量最近assign后的值
即不允许某个变量在工作内存中改变以后没有同步回主存 - 不允许一个线程把某个未发生任何
assign
操作的变量同步回主存 - 新的变量只能诞生在主存中, 不允许工作内存使用一个未被初始化的变量. 换句话说就是, 要想对一个变量执行
use(执行引擎拷贝工作内存的某个值)
和store(主内存拷贝工作内存的某个值)
操作, 必须先在该变量上执行assign(执行引擎拷贝到工作内存)
和load(主内存拷贝到工作内存)
- 同一时刻, 某个变量只允许一个线程对其
lock
, 但可以被同一个线程多次lock
. 多次执行lock
后, 只有执行相同次数的unlock
, 该变量才会被解锁 - 如果要对一个变量
lock
, 则必须先清空该变量在工作内存的值, 在执行引擎使用该变量前, 需要重新执行load
和assign
操作在工作内存中初始化该变量 - 如果一个变量没有被
lock
过, 则不允许被unlock
; 一个线程不能去unlock
一个被其它线程lock
的变量 - 一个变量在执行
unlock
之前, 必须先把该变量同步会主内存(即执行store
,write
操作)
-
这8个规则再加上后面的volatile特殊规则
, 就完全确定了Java程序中哪些内存访问动作在并发下时安全的
网友评论