学习参考资料:周志明老师的著作《深入理解Java虚拟机(第3版)》
1.Java内存模型
需要注意Java内存模型指的是JMM,而不是运行时区域。
1.1主内存和工作内存
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细节。
内存模型如下图,线程运行时会将代码所需要的运行时数据从工作内存中获取,所以工作内存会从主内存中复制相关数据。那么也就是如果两个线程同时复制了主内存的共享变量并进行了修改,如果不加以控制,可能就会发生并发问题。
1.2内存间的交互
对于变量如何从主内存拷贝到工作内存和工作内存同步回主内存中,Java内存模型定义了下面8种操作,每一种操作都具有原子性。
lock ( 锁定):作用于主内存的变量, 它把一个变量标识为一条线程独占的状态。unlock ( 解锁):作用于主内存的变量, 它把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定。read ( 读取):作用于主内存的变量, 它把一个变量的值从主内存传输到线程的工作内存中, 以便随后的load动作使用。load ( 载入):作用于工作内存的变量, 它把read操作从主内存中得到的变量值放入工作内存的变量副本中。use ( 使用):作用于工作内存的变量, 它把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。assign ( 赋值):作用于工作内存的变量, 它把一个从执行引擎接收到的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store ( 存储):作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中, 以便随后的write操作使用。write ( 写入):作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中。
Java内存模型还规定了在执行上述基本操作时必须满足一下规则(了解):
- 不允许read和load、 store和write操作之一单独出现, 即不允许一个变量从主内存读取了但工作内存不接受,
或者从工作内存发起回写了但主内存不接受的情况出现。 - 不允许一个线程丢弃它的最近的assign操作, 即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地( 没有发生过任何assign操作) 把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“ 诞生” , 不允许在工作内存中直接使用一个未被初始化( load或assign) 的变量, 换句话说, 就是对一个变量实施use、 store操作之前, 必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作, 但lock操作可以被同一条线程重复执行多次, 多次执行lock后, 只有执行相同次数的unlock操作, 变量才会被解锁。
- 如果对一个变量执行lock操作, 那将会清空工作内存中此变量的值, 在执行引擎使用这个变量前, 需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定, 那就不允许对它执行unlock操作, 也不允许去unlock一个被其他线程锁定住的变量。
- 对一个变量执行unlock操作之前, 必须先把此变量同步回主内存中( 执行store、 write操作) 。
1.3对于volatile型变量的特殊规则
Java内存模型为volatile专门定义了一些特殊的访问规则,当一个变量被定义成volatile之后,他将具有可见性和有序性两个特征。
import java.util.Date;
public class VolatileTest {
public static boolean k = true;
public static void main(String[] args) throws InterruptedException {
new Thread(){
@Override
public void run() {
Date date = new Date();
while(k){
}
System.out.println("===success===");
}
}.start();
Thread.sleep(100);
new Thread(){
@Override
public void run() {
k=false;
System.out.println("修改为false");
for (int time = 0; time < 60; time++) {
System.out.println(time +"s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
未被volatile修饰,7s后未感知到
被volatile修饰后,很快就被感知到
- volatile的有序性也就是指禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
对 volatile 变量的特殊规则:
- 某个线程对 volatile 变量进行use前必须进行连续的 read、load 操作。这条规则要求在工作内存中,每次使用变量前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量所做的修改后的值。
- 某个线程对 volatile 变量进行assign后必须进行连续的 store、write 操作。这条规则要求在工作内存中,每次修改变量后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改。
- 如果线程 A 的 use、assign 操作先于线程 B,那么线程 A 的 read、write 也必须先于 线程 B。这条规则要求 volatile 变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
1.4原子性、有序性、可见性
Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的。
(1)原子性
- 基本数据类型的访问读写具备原子性(不考虑 long、double 的非原子性协定): Java 内存模型直接保证了 read、load、assign、use、store 和 write 操作的原子性。
- synchronized 代码块之间的操作具备原子性:底层通过 lock 和 unlock 操作实现。
(2)可见性 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
volatile、synchronized、final 关键字都能实现可见性。
(3)有序性 Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
2.Java与线程
-
java中的线程采用的是内核线程实现。 内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。 每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。 程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是通常意义上所讲的线程,每个轻量级进程都由一个内核线程支持。这种轻量级进程与内核线程之间 1:1 的关系称为一对一的线程模型。 -
java线程调度(抢占式) 线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度和抢占式线程调度(java采用这一种)。 (1)协同式线程调度 线程的执行时间由线程本身来控制,线程执行完之后,主动通知系统切换到另外一个线程上。 协同式线程调度最大的好处是实现简单,而且切换线程的操作对线程自己是可知的,所以没有什么线程同步的问题。它的坏处就是线程执行时间不可控,如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。 (2)抢占式线程调度 每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。 使用抢占式线程调度时,线程的执行时间是系统可控的,不会有一个线程导致整个进程阻塞的问题。 Java 使用的线程调度方式就是抢占式调度。 -
虽然1:1的内核线程模型是如今Java虚拟机的主流选择,但是这种映射到操作系统上的线程天然缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。 为什么说调度成本高昂? 内核线程的调度成本主要来源于用户态与核心态之间的转换,而这两种转换的开销主要来自于中断、保护和恢复现场的成本。
后面还会陆陆续续更新这系列的读书笔记,期待您的关注~~
|