深入理解Java虚拟机读书笔记--第十二章 Java 内存模型与线程
第十二章Java内存模型与线程
物理机为了尽可能利用多处理器, 采取了以下措施:
1. 高速缓存
2. 对代码进行乱序执行优化
高速缓存解决了处理器与内存的速度矛盾(参考wiki 说明: https://zh.wikipedia.org/wiki/CPU%E7%BC%93%E5%AD%98)
但是引入了一个新的问题, 缓存一致性。 每个处理器都有自己的高速缓存, 而他们又共享同一主内存, 当缓存数据不一致, 同步回主内存时以谁的缓存数据为准呢?
所以各处理器访问缓存时要遵循一类缓存一致性协议。
Java 的内存模型与物理机类似:
Java 内存模型规定了所有变量存储在主内存中, 每个线程又自己的工作内存, 工作内存中保存了被该线程使用到的变量的主内存副本拷贝。 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
工作内存与主内存直接的交互,Java内存模型定义了以下8中操作来完成(虚拟机实现保证下面的操作都是原子的,不可再分的):
1. Lock , 作用于主内存的变量, 把一个变量标识为一条线程独占的
2. Unlock, 作用于主内存的变量, 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3. Read, 作用于主内存的变量, 把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用
4. Load, 作用于工作内存中的变量, 把read 操作从主内存得到的变量值放入工作内存的变量副本中
5. Use, 作用于工作内存中的变量,将工作内存中的变量传递给执行引擎, 每当虚拟机遇到一个需要使用到变量的值得字节码指令时会执行这个操作
6. Assign, 作用于工作内存的变量, 把一个从执行引擎接受到的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的的字节码指令时执行这个操作
7. Store,作用于工作内存中的变量, 将工作内存中的变量值传送到主内存中,以便随后的write使用
8. Write,作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
Volatile 的语义:
1. 保证变量对所有线程的可见性
当一条线程修改了这个变量的值,新值对其他线程是可以立即得知的
Volatile 不能保证原子性, 因为Java 里面的运算不是原子操作。
运算结果不依赖变量的当前值得场景可以使用volatile来保证原子性, 一般是对域变量的get/set. 当一个变量声明为volatile , 则对该域的简单读写是线程安全的。
2. 进制指令重排序优化
原子性,可见性与有序性:
原子性:
1. 基本数据类型的读写是具备原子性的
处理器保证基本的内存操作是原子性的, 即读写一个字节是原子性的。64位的long和double虽然有非原子性协定, 但几乎所有虚拟机都会把64位数据的读写操作实现为具有原子性的操作。
2. Synchronized 快直接的操作也具有原子性
可见性:
1. Volatile 修改的变量具有可见性
2. Synchronized 和final 也能保证可见性
有序性:
如果在本线程内观察, 所有操作都是有序的; 如果在另一个线程中观察一个线程, 所有操作都是无序的。
Volatile 和synchronized可以保证有序性,volatile 本身有禁止指令重排序的语义。
先行发生原则(happens-before):
如果说操作A先行发生于操作B, 其实就是说在发生操作B之前, 操作A的影响能被操作B观察到。 “影响”保证修改了内存中共享变量的值,发送了消息,调用了方法等。
Java 内存模型下“天然”的先行发生关系:
1. 程序次序规则: 在一个线程内, 按照代码顺序, 书写在前面的操作先行发生于书写在后面的操作, 即控制流顺序。
2. 管程锁定规则: 一个unlock操作先行发生于后面对同一个锁的Lock操作。
3. Volatile 变量规则: 对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4. 线程启动规则: Thread对象的start()方法先行发生于此线程的每一个工作。
5. 线程终止规则: 线程中的所有操作都先发生于对此线程的终止检测
6. 线程中断规则: 对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7. 对象终结规则: 一个对象的初始化完成先行发生于它的finalize()方法的开始
8. 传递性: 如果操作A先行发生于操作B, 操作B先行发生于操作C, 那么操作A先行发生于操作C。
一个操作“时间上的先发生”不代表这个操作是“先行发生”, 反之也是如此。
Java 线程实现:
1. 1:1 (内核线程)
每一个线程(轻量级进程)都由一个内核线程支持
优点: 一个线程阻塞不会影响其他
缺点: 需要在用户态和内核态之间来回切换, 系统调用代价较高
2. N:1(用户线程)
进程与用户线程间1:N 的关系
优点: 完全用户态, 线程调度低消耗, 可以支持大规模的线程数量
缺点: 没有内核参与, 线程的调度,阻塞处理等实现非常复杂
3. N:M (混合型)
即存在用户线程, 又存在轻量级进程。
Sun JDK 中, Windows 版本和Linux 版本都使用的是一对一的线程模型, 一条Java 线程映射到一条轻量级进程(内核线程)中。
线程调度:
1. 协同式调度
线程执行时候由线程自己控制, 线程工作执行完之后, 主动通知系统切换到另一个线程上
优点: 没有线程同步问题, 因为切换操作对线程是可知的
缺点: 线程执行时间不可控, 如果一个线程有问题, 程序会一直阻塞
2. 抢占式调度
由系统分配执行时间, 线程切换由系统控制
Java 使用的是抢占式线程调度方式。
线程优先级不太靠谱,不应该依赖:
1. 线程优先级映射到系统优先级,会存在多个优先级在系统中是同一个优先级情况
2. 优先级可能会被系统改变