<1> 多线程中
1.1 反复调用同一个线程的 start 方法是否可行?
1.2 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
Thread中start()方法
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
// threadStatus != 0 表示这个线程已经被启动过或已经结束了
// 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将这个线程添加到当前线程的线程组中
group.add(this);
// 声明一个变量,用于记录线程是否启动成功
boolean started = false;
try {
// 使用native方法启动这个线程
start0();
// 如果没有抛出异常,那么started被设为true,表示线程启动成功
started = true;OUUNFZ } finally {
// 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
try {
// 如果线程没有启动成功,就从线程组中移除这个线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
// 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
}
}
}
结合上面的源码可以得到的答案是: 都不行
调用 start 之后,threadStatus 的值会改变(threadStatus !=0),
再次调用 start 方法会抛出 IllegalThreadStateException 异常。
threadStatus 为 2 代表当前线程状态为 TERMINATED
<2> 各个状态转换
<3> 上下文切换
Switching between threads for concurrent execution
Involes saving concurrent threads' context and loading another thread's context
Java provides tools like the Thread class for managing threads and enabling context switching.
一般减少上下文切换的方法有:
3.1 无锁并发编程:可以参照 ConcurrentHashMap 锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
3.2 CAS 算法,利用 Atomic + CAS 算法来更新数据,采用乐观锁的方式,可以有效减少一部分不必要的锁竞争带来的上下文切换。
3.3 使用最少线程:避免创建不必要的线程,如果任务很少,但创建了很多的线程,这样就会造成大量的线程都处于等待状态。
3.4 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
<4> JMM 与 happens-before
一方面,我们开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。
JMM 考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。
对于我们开发者来说,JMM 提供了happens-before 规则(JSR-133 规范),满足了我们的诉求——简单易懂,并且提供了足够强的内存可见性保证。 换言之,我们开发者只要遵循 happens-before 规则,那么我们写的程序就能保证在 JMM 中具有强的内存可见性。
JMM 使用 happens-before 的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程内,也可以是不同的线程种。
happens-before 关系的定义如下:
- 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。
happens-before 关系本质上和 as-if-serial 语义是一回事。
as-if-serial 语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的,happens-before 关系保证正确同步的多线程程序的执行结果不被重排序改变。
总之,如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。
happens-before 关系有哪些?
在 Java 中,有以下天然的 happens-before 关系:
-
程序顺序规则:一个线程中的每一个操作,happens-before 于该线程中的任意后续操作。
-
监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
-
volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
-
传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
-
start 规则:如果线程 A 执行操作
ThreadB.start()
启动线程 B,那么 A 线程的ThreadB.start()
操作 happens-before 于线程 B 中的任意操作。 -
join 规则:如果线程 A 执行操作
ThreadB.join()
并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从ThreadB.join()
操作成功返回。
<5> volatile
使用 volatile 关键字修饰共享变量可以禁止这种重排序。怎么做到的呢?
当我们使用 volatile 关键字来修饰一个变量时,
Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:
1. 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
2. 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。
换句话说:
当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定已经全部进行,且结果对后面的操作可见;
在其后面的操作肯定还没有进行; 在进行指令优化时,不能将 volatile 变量的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
也就是说,执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见