3. 先行发生原则(happens-before)
① 先行发生原则概述
- 先行发生原则(
happens-before
)可以用于判断数据是否存在竞争、线程是否安全的主要依据。 - 先行发生: JMM中定义的两项操作的偏序关系,如果操作A先行发生于操作B,则操作A产生的影响能被操作B观察到。影响包括修改共享变量的值、发送了消息、调用了方法等。
- 先行发生的简单示例:
- 线程A的
i = 12
的操作先行发生于线程B的j = i
操作,则变量j
的值一定为12。 - 如果加入线程C的操作
i = 24
,它与线程B的j = i
操作不存在先行发生关系,则变量j
的值就变得不确定,可能是12,也可能是24。
// 在线程A中执行的操作
i = 12;
// 在线程B中执行的操作
j = i;
// 在线程C中执行的操作
i= 24;
- 除了可以使用
synchronized
和volatile
关键字保证有序性,其中synchronized
关键字,要求一个变量同一时刻只允许一个线程对其进行lock
操作;volatile
关键字,运算结果不依赖于当前变量的值,可以保证线程安全。 - 先行发生原则,可以让一个操作无须控制就能先于另一个操作发生,即无须任何同步措施就能成立的操作的先后关系。
② 先行发生原则
- 程序顺序规则(
Programma Order Rule
):一个线程内,书写在前面的操作先行发生于书写在后面的操作。
- 监视器锁规则(
Monitor Lock Rule
):对一个锁的unlock
操作先行发生于后面
对该锁的lock
操作。后面是指时间上的先后顺序。
- volatile变量规则(
Volatile Variable Rule
):对一个volatile
变量的写操作先行发生于后面
对变量的读操作。
- 线程启动规则(
Thread Start Rule
):线程A通过threadB.start()
启动线程B,则线程A中的threadB.start()
操作先行发生于线程B中的任意操作。
- 线程加入规则(
Thread Join Rule
):线程A调用threadB.join()
操作并成功返回,则线程B中的任意操作都先行发生于线程A从threadB.join()
操作成功返回。
- 线程中断规则(
Thread Interruption Rule
):对一个线程的中断操作先行发生于该线程的代码检测到中断事件的发生。中断操作通过调用interrupt()
方法实现,中断检测通过调用interrupted()
方法实现。
public class InterruptRule {
public static void main(String[] args) {
MyThread threadB = new MyThread("threadB");
threadB.start();
System.out.println("主线程调用 threadB.interrupt()方法,中断threadB");
threadB.interrupt();
}
}
class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
while (!interrupted()) {
System.out.println("线程" + name + "已被中断");
}
}
}
- 对象终结规则(
Finalizer Rule
):一个对象的初始化完成(即构造函数的结束)先行发生于它的finalize()
方法的开始。
- 传递性(
Transitivity
):如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。 - 时间上的先后顺序与先行发生的关系:
- 时间上的先后顺序并不保证先行发生。
① 下面的代码,线程A先调用了setValue()
方法,线程B再调用getValue()
方法。看起来应该是线程B再调用getValue()
方法返回1,实际却很有可能返回0.
②因为setValue()
方法和getValue()
方法,并不满足任一的先行发生规则,无法保证线程安全。
public class TimeBefore {
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static void main(String[] args) {
TimeBefore test = new TimeBefore();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test.setValue(1);
System.out.println("setValue:" + 1);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("getValue:" + test.getValue());
}
});
thread1.start();
thread2.start();
}
}
2. 同样,先行发生并不保证时间上的先后,如int i =1; int j = 2;
,按照程序顺序规则,int i =1
操作先行发生于int j = 2
操作。但实际执行时,可能 int j = 2
操作可能先被处理器执行。
3. 衡量多线程并发访问的安全问题时,不能受时间先后的迷惑,一切必须以先行发生原则为判断依据。
4. Java的线程
- 线程是CPU调度的基本单位,又称为轻量级进程(
LWP
)。 - 线程主要有3种实现方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程(
LWP
)混合实现。 - 几个常见概念:
- 内核线程(Kernel Level Thread,
KLT
): 由操作系统内核支持的线程,内核通过调度器(Scheduler
)对内核线程进行调度,将其映射到各个CPU上进行执行。 - 轻量级进程(Light Weight Process,
LWP
):程序一般不直接使用内核线程,而是使用内核线程的高级接口——轻量级进程,即我们通常意义上所说的线程。 - 用户线程(User Level Thread,
ULT
):广义上讲,只要不是内核线程的线程都是用户线程,包括轻量级进程;狭义上讲,用户线程是指完全建立在用户空间线程库上,线程的建立、同步、销毁和调度都在用户态中完成,不需要内核的帮助。
① 使用内核线程实现
- 内核线程实现的方式:
- 一个进程分解为若干个轻量级线程,每个轻量级线程与内核线程是
1:1
映射关系。 - 内核线程调度器对内核线程进行调度,将其映射到各个CPU上进行执行。
- 轻量级进程的局限性:
- 轻量级进程基于内核线程实现,任何线程操作都需要进行系统调用,而系统调用代价很高,需要在用户态和内核态中来回切换。
- 每个轻量级进程都需要一个内核线程,因此轻量级进程需要消耗一定的内核资源,导致系统能支持的轻量级进程是有限的。
② 使用用户线程实现
- 使用用户线程的实现方式:
- 若干个用户线程构成一个进程,进程在CPU上执行。
- 用户线程和进程之间,是
N:1
的映射关系。
- 使用用户线程的优势在于不需要内核支援,劣势也在于不需要内核支援。
- 因为不需要使用内核支援,所有线程操作都需要自己处理,实现起来一般比价复杂。
- 使用用户线程的程序越来越少了,Java、Ruby语言之前都是使用用户线程的,后来又放弃使用它。
③ 使用用户线程加轻量级进程混合实现
- 混合实现方式的特点:
- 既存在用户线程,又存在轻量级进程。
- 用户线程的执行还是完全建立在用户空间,可以支持大规模的用户线程并发。
- 轻量级进程是用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度和CPU映射,还可以降低进程被阻塞的风险。
- 用户线程和轻量级进程之间是
N:M
的映射关系。
④ java线程的实现方式
Sun JDK
的Windows
和Linux
版本都是使用内核线程方式实现的,即使用一对一的线程模型。- 因为
Windows
和Linux
系统提供的线程模型就是一对一的。
5. Java线程的优先级
① 线程调度两种方式
- 线程的调度有协同式调度(Cooperative Threads-Schedule)和抢占式调度(Preemptive Threads-Schedule)
- 协同式调度: 线程的执行时间由线程本身控制,当前线程将自己的任务执行完成,才会通知系统切换到另外一个线程上。
- 线程切换对线程自己是可知的,不存在同步问题。
- 由于线程的执行时间不可控的,很容易出现一个线程一直不通知系统进行线程切换,从而导致整个程序阻塞在当前线程。
- 抢占式调度: 每个线程由系统进行时间分配,线程切换不由线程本身决定(
Thread.yield()
方法可以让出执行时间)。
- Java使用的就是抢占式调度。
- 抢占式调度模式,线程的执行时间是系统可控的,不会出现一个线程导致整个程序阻塞的情况。
② Java线程的优先级
- Java线程提供优先级设置,优先级较高的线程被调度执行的机会更大。相比优先级较低的线程,系统为它分配的执行时间更长一点。
- 通过
thread.setPriority(int priority)
设置线程的优先级,priority可以是1 ~ 10
的数字。 - Java提供10个级别的线程优先级,
MIN_PRIORITY
至MAX_PRIORITY
。其中MIN_PRIORITY = 1
,NORM_PRIORITY = 5
,MAX_PRIORITY = 10
。
- Java线程的优先级并不靠谱:
- 原因: Java线程最终会映射成系统的原生线程,线程的调度最终还是取决于操作系统。
- 不靠谱的表现1: Java的线程优先级与某些平台的优先级并不是一一对应的,比如Windows平台有7种线程优先级,这就导致不同的Java线程优先级最终可能变得相同。
- 不靠谱的表现2: Java线程的优先级可能被系统自行更改
- 综上,不能通过Java线程的优先级准确的判断都处于Ready状态的线程,谁会先被系统调度执行。