1 前言
2 Synchronization
3 Wait Sets and Notification
3.1 Wait
3.2 Notification
3.3 Interruptions
3.4 Interactions of Waits, Notification, and Interruption
4 Sleep and Yield
调用Thread.sleep方法触发当前的运行线程T从执行状态转变为睡眠状态(暂停执行),睡眠状态的延续时间由输入参数确定,其使用JVM系统的计时器(timers)以及调度器(schedulers)确定T的睡眠时间的精确度以及准确度。在睡眠期间T不释放已占用的资源,也就是,T在睡眠期间继续占用与持有被执行对象的监听器(monitors),在系统资源充足的情况下T在睡眠时间段结束时被唤醒,系统的调度器从处理器资源中分配可用的时间片给T,T重新获取时间片并且恢复之前的状态继续执行,在系统资源不足的情况下,当睡眠时间段结束时,T将在什么时间点从睡眠状态中被唤醒是由系统的调度器决定。
JVM的同步语义规定,调用Thread.sleep与Thread.yield方法之前不会将线程本地寄存器中缓冲刷到共享内存区域,在调用Thread.sleep与Thread.yield方法之后不会重新加载共享内存区域的缓冲到线程本地寄存器。例如如下的代码所示:
while (!this.done) Thread.sleep(1000); |
在以上代码片段中,假设this.done在执行对象中被声明为非volatile类型,编译器是只读取一次this.done的值,在每次while循环中都使用第一次读取到的值,即使其他线程修改了this.done的值。同理,自身线程修改了this.done的值,只影响当前线程而不会影响其他线程中while的循环逻辑。
Thread.sleep与Thread.yield的区别如下所示:
|
5 Memory Model
内存模型,用于描述程序段以及该程序段的执行轨迹,并且确定该程序段的执行轨迹是否合法。因此,JVM的内存模型是用于检测程序段的执行轨迹中的每一次读操作,并且确保对该次读操作所涉及到的对象或者变量的写操作是否合法,这些规则或者语义被称之为内存模型。
内存模型描述一个程序段可能存在的行为,理论上,实现相同业务逻辑的代码可以有很多种写法,但是执行这些代码逻辑的输出结果,对于内存模型来说都是可预测的,不可预测则不合法。
因此,内存模型在JVM的作用非常明显,在代码的编译阶段或者处理器执行阶段可以优化代码的执行轨迹,提升代码的运行效率,代码执行轨迹的优化包括对代码执行步骤的重新排序(指令重排序)或者删除代码执行轨迹中一些不必要的同步锁。
如下举例说明多线程的执行轨迹对程序段中的共享变量的影响:
假设r1、r2是本地变量,A、B是共享变量,初始化A == B == 0 |
Thread 1 1: r2 = A; 2: B = 1; |
Thread 2 3: r1 = B; 4: A = 2; |
对于以上代码片段的线程Thread1与Thread2是以多线程并行的方式执行,其中序号1、2、3、4表示指令的编号,直观地,输出的结果不太可能是r2=2、r1=1,指令1与指令3应该首先被执行,因此指令2与指令4不可能对指令1与指令3可见。现在假设,对以上的执行轨迹重新排序,指令4在指令1之前执行、指令1在指令2之前执行、指令2在指令3之前执行、指令4在指令1之前执行,则该重新排序不成立,因为指令4在指令1之前执行与指令4在指令1之前执行两个步骤发生矛盾。 然而,JVM的编译器能分别对每个线程内部的执行轨迹重新排序,而不是对所有线程的执行步骤一起重新排序,该机制确保线程之间的相互独立,则每个线程重新排序后如下所示: |
Thread 1 1: B = 1; 2: r2 = A; |
Thread 2 3: r1 = B; 4: A = 2; |
执行重新排序之后,在多线程的运行环境中,系统指令的调度顺序可能是:执行指令1、执行指令3、执行指令4、执行指令2,输出结果非常有可能是r2=2、r1=1 |
JVM提供JIT(Just-In-Time)即时编译机制支持指令重新排序,JIT是一种优化指令执行的技术,能提升指令的执行效率,提供在运行时阶段对代码执行即时的重新编译的功能,JVM使用JIT机制与多级内存相结合实现指令级别的重新排序 |
以下举例说明在多线程并发环境中共享变量的前向替换的问题:
p、q是共享对象,初始化p == q 与p.x == 0 |
Thread 1 r1 = p; r2 = r1.x; r3 = q; r4 = r3.x; r5 = r1.x; |
Thread 2 r6 = p; r6.x = 3; |
以上的代码片段,编译器优化后如下所示,其中r2、r5没有使用中间对象r1,而是r2直接赋值r1.x,r5直接赋值r2: |
Thread 1 r1 = p; r2 = r1.x; r3 = q; r4 = r3.x; r5 = r2; |
Thread 2 r6 = p; r6.x = 3; |
以上的代码片段,在多线程的运行环境中,假设Thread 2的r6.x=3赋值发生在Thread 1的r2与r4的读取步骤之间,则输出的结果是r2与r5的值是0,而r4的值是3,因此,从代码片段直观地看,p.x的值是从0变为3再变回0,也就是,编译器运用前向替换的操作使本地变量r5重复利用之前的本地变量r2 |
由以上的分析可知,内存模型确定了程序段中每个执行步骤应该读取什么样的值,也就是,相互隔离独立的每个线程的每个执行步骤都必须遵守线程的语义与规则,但是每个执行步骤中涉及到的读取的值是由内存模型确定,这些规定被称之为线程内部语义,该语义制约的范围是单个线程内部的执行步骤,并且根据内存可见范围内的读取到的值实行整体预测线程内的执行步骤。例如,为了确定线程T在一次运行中的执行步骤是否合法,JVM会根据线程T的上下文环境估算T的实现,每次估算会产生一个合法的执行步骤,假设存在其中一个执行步骤a,根据程序段的顺序确定a的下一执行步骤是b,则a必须完全匹配b,如果b是读取操作,则JVM内存模型使用b读取的值继续预测b的下一执行步骤c,由这些abc组成的执行步骤是JVM内存模型预测的合法的执行步骤。
(未完待续)