深入理解 Java 虚拟机:Java 内存模型与线程
深入理解 Java 虚拟机:Java 内存模型与线程什么是内存模型?什么是高速缓存?缓存一致性Java 内存模型主内存工作内存内存间的交互操作volatile 关键字对所有线程可见性禁止指令重排序Java 内存模型的特征原子性可见性有序性synchronized 缺点先行发生原则“天然的” 先行发生关系什么是内存模型?内存模型: 可以理解为,在特定的操作协议下,对特点的内存或高速缓存进行读写访问的
深入理解 Java 虚拟机:Java 内存模型与线程
什么是内存模型?
内存模型: 可以理解为,在特定的操作协议下,对特点的内存
或高速缓存
进行读写访问
的过程的抽象
。
什么是高速缓存?
在运算时,将需要使用到的数据从内存复制到缓存(Cache)中
,以此让计算能更快的进行
,计算结束后再从缓存同步回内存中
,这样就无需频繁的等待缓慢的内存读写
。
缓存一致性
缓存一致性: 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。
Java 内存模型
主内存
主内存是虚拟机内存的一部分。
Java 内存模型规定所的变量都存储在主内存中。
变量数据: 最根源的数据,线程不能直接操作。
工作内存
工作内存中保存了被该线程使用到的变量
的主内存副本拷贝
,线程对变量的所有操作(读取,赋值等)
,都必须在工作内存中进行
,不能直接读写主内存中的数据。不同线程间也无法直接访问对方工作线程中的变量
。
变量数据: 从主内存中拷贝来的副本,其他线程无法访问,操作结束后需写回主内存。
内存间的交互操作
虚拟机实现时,必须保证下面提及的每一种操作都是原子的
、不可再分的
。
- lock(锁定): 作用于
主内存
的变量,把变量标记为某个线程独占。 - unlock(解锁): 作用于
主内存
的变量,释放一个锁定的变量。 - read(读取): 作用于
主内存
的变量,把一个变量的值从主内存传输到工作内存
中,以便后续load
动作使用。 - load(载入): 作用于
工作内存
的变量,将read
过来的值放入工作内存
的变量副本
中。 - use(使用): 作用于
工作内存
的变量,把工作内存
中存在的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用变量的值时就会执行这个操作。 - assign(赋值): 作用于
工作内存
的变量,把从执行引擎接收到的值,赋值给工作内存
中的一个变量,当虚拟机遇到赋值操作时候执行操作。 - store(存储): 作用于
工作内存
的变量,把工作内存
中的一个变量传递给主内存,一遍后续write
动作使用。 - write(写入): 作用于
主内存
的变量,把store
操作的值放入主内存的变量中。
将变量从主内存复制到工作内存: 顺序执行 read
和 load
,只需按顺序即可,中间可插入其他指令。
从工作内存写回主内存: 顺序执行 sore
和 write
,只需按顺序即可,中间可插入其他指令。
volatile 关键字
对所有线程可见性
特性: 保证此变量对所有线程的可见性
,当一条线程修改了这个变量值,其他线程会立即收到通知。
volatile 关键字保证线程可见,不代表线程安全:
以下为 race ++
的反编译结果
volatile 关键字
保证了线程可见性,因此只要去读数据,就可以拿到正确的数据
而这里的读操作,只有 getstatic
字节码指令,将数据读入栈顶(每个线程各自拥有,对外部可见)
而后续的不管是递增 iadd
还是回写回主内存 putstatic
指令之前都没有再去读数据
因此如果这期间如果数据发生了变更
,在栈顶的数据就成了旧数据
,回写回主内存就会产生覆盖
,线程不安全
。
禁止指令重排序
普通变量仅仅会保证在该方法的执行过程中,所有依赖赋值结果的地方都能获取到正确的结果
,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致
。
这么做的目的是一种机器级的优化操作,使得某个部分的汇编代码被提前执行
。
volatile
可以避免这种的优化操作。
内存屏障:
volatile
是通过 内存屏障
来防止指令重排序的,反编译后即下图红色框框中的部分。
作用: 防止 内存屏障
后面的指令排序到 内存屏障
之前。
Java 内存模型的特征
Java 内存模型是围绕 并发过程中
如何处理 原子性
、可见性
和 有序性
3 个特征来建立的。
原子性
直接 保证原子性的操作
包括:read
、load
、assign
、use
、store
、write
基本数据类型的访问读写都是具备原子性的
例外的只有 long
和 double
,具有非原子协定,但是各大虚拟机实现时,都把他们的读写操作作为原子操作来对待,因此知道下就好了,不需要太在意。
为了保证原子性,虚拟机提供字节码 monitorenter
和 monitorexit
来操作 lock
和 unlock
,反应到代码层面就是 synchronized
关键字,因此 synchronized
块的操作也具有原子性。
可见性
可见性: 可见性是指,当一个线程修改了共享变量的值,其他变量能立即得知这个修改。
保证可见性的关键字: volatile
、synchronized
、final
。
有序性
有序性: 如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无需的。前半句是指 “线程内表现为串行的语义”,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。
防止指令重排序关键字: volatile
、synchronized
。
synchronized 缺点
synchronized
同时满足 3 大特性,看起来十分 “万能”,但过度滥用回导致性能问题。
先行发生原则
即多线程同时操作一个变量导致值不正确的线程不安全问题。
“天然的” 先行发生关系
即不需要同步器,在代码中能直接使用
- 程序次序规则: 在一个线程内,按照程序代码的顺序,写在前面的代码比写在后面的代码先执行。
- 管程锁定规则: 同一个锁,一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。不解锁就没法再锁定。
- volatile 变量规则: 对一个
volatile
变量的写操作优先于后面这个变量的读操作,“后面” 是指时间上的先后顺序。 - 线程启动规则: Thread 对象的 start() 方法先行发生于此线程的每个动作。不启动线程,其他动作无法进行。
- 线程终止规则: 线程中所有操作都优先于此线程的终止检查,可通过
Thread.join() 方法结束
、Thread.isAlive() 返回值
等手段检查到线程已经终止执行。 - 线程中断规则: 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检查到终端事件的发生,可通过
Thread.interrupt()
方法检查到是否有中断发生。 - 对象终结规则: 一个对象的初始化完成(构造函数执行结束),先行发生于它的 finalize() 方法的发送
- 传递性: 操作 A 优先于操作 B,操作 B 优先于操作 C,操作 A 就必然优先于 操作 C
Java 线程状态
- 新建(New): 创建后
尚未启动
的线程状态 - 运行(Runable): 包含了操作系统线程状态中的
Runable
和Ready
状态,处于此状态下的线程可能处于正在执行
或者等待 CPU 分配执行事件
。 - 无期限等待(Waiting): 此状态的线程
不会被分配执行事件
,需要等待被其他线程唤醒
。以下方法会让线程进入Waiting
。
没有设置Timeout
参数的Object.wait()
方法。
没有设置Timeout
参数的Object.join()
方法。
LockSupport.park()
方法。 - 限期等待(Time Waiting): 此状态下线程
不会被分配 CPU 执行时间,不过无需其他线程显式的唤醒,在一定时间后由系统自动唤醒
。以下方法会让线程进入Time Waiting
。
Thread.sleep()
。
设置Timeout
参数的Object.wait()
方法。
设置Timeout
参数的Object.join()
方法。
LockSupport.parkNanos()
方法。
LockSupport.parkUnit()
方法。 - 阻塞(Blocked):
“阻塞状态”
在等待获取一个排他锁
,需要另一个线程放弃这个锁的时候发生
。 - 结束(Terminated):
已终止
线程的状态,现在已经结束执行
。
更多推荐
所有评论(0)