三、多线程&并发篇
3.1 Java实现多线程的几种方式
1. 直接使用Thread
Thread t = new Thread(){
@Override
public void run() {
log.info("t线程执行了...");
}
};
t.setName("t");
log.info("主线程执行了...");
t.start();
16:13:46.168 [main] INFO com.gor.concurrent.test.Test -- 主线程执行了...
16:13:46.171 [t] INFO com.gor.concurrent.test.Test -- t线程执行了...
2. 使用Runnable配合Thraed
Runnable runnable = new Runnable() {
@Override
public void run() {
log.info("t线程执行了...");
}
};
Thread t = new Thread(runnable);
t.setName("t");
t.start();
log.info("主线程执行了...");
16:18:36.615 [main] INFO com.gor.concurrent.test.Test -- 主线程执行了...
16:18:36.616 [t] INFO com.gor.concurrent.test.Test -- t线程执行了...
Java8之后可以使用lambda精简代码
Thread t = new Thread(() -> {
log.info("t线程执行了...");
}, "t");
t.start();
log.info("主线程执行了...");
16:20:52.278 [main] INFO com.gor.concurrent.test.Test -- 主线程执行了...
16:20:52.278 [t] INFO com.gor.concurrent.test.Test -- t线程执行了...
3. FutureTask配合Thread
FutureTask能够接受Callable类型的参数,用来处理有结果的情况
FutureTask<String> task = new FutureTask<>(() -> {
log.info("t线程执行了...");
return "successfully executed...";
});
new Thread(task, "t").start();
// 主线程阻塞,等待返回结果
String res = task.get();
log.info("t线程执行完毕,结果是: {}", res);
16:29:20.186 [t] INFO com.gor.concurrent.test.Test -- t线程执行了...
16:29:20.189 [main] INFO com.gor.concurrent.test.Test -- t线程执行完毕,结果是: successfully executed...
3.2 查看进程线程的方法
1. windows
- tasklist 查看进程
- taskkill /F /PID [PID] 杀死进程
2. Java
jps查看所有Java进程jstack <PID>查看某个Java进程的所有线程状态jconsole来查看某个Java进程中线程的运行情况
3.3 线程状态
1. 操作系统层面
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统关联),可以由CPU调度执行
- 【运行状态】指获取了CPU时间片运行中的状态,当时间片用完会转换至【可运行状态】
- 【阻塞状态】调用了阻塞API会进入阻塞状态,操作完毕会唤醒线程,转换至【可运行状态】
- 【终止状态】线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
2. Java层面
根据Thread.State枚举,分为六种状态
3.4 常用方法
Java 中线程的常用方法分为两类:Thread 类的方法 和 Object 类的方法。以下是一些常用方法及其解释:
1. Thread 类中的常用方法
-
start():启动线程,线程进入就绪状态,等待 CPU 调度。start()方法会调用run()方法执行线程的任务。 -
run():线程的执行逻辑。run()方法内定义线程要完成的任务,一般通过继承Thread类或实现Runnable接口来覆盖这个方法。 -
sleep(long millis):使当前线程进入休眠状态,暂停执行指定的时间millis毫秒。期间不会释放锁,可以用来模拟延迟。 -
join():等待线程终止。当前线程会等待调用了join()的线程执行完毕后再继续执行,常用于确保某个线程执行结束后再进行下一步操作。 -
yield():提示让出 CPU 执行权,重新进入就绪状态。它会让当前线程让出时间片,但并不一定会立即暂停执行,因为具体行为依赖于操作系统的线程调度。 -
interrupt():中断线程。如果线程在sleep()、wait()或join()状态时调用该方法,线程会抛出InterruptedException。可以用来通知线程提前终止。 -
isAlive():判断线程是否还在活动(是否已经启动并且未终止)。 -
setDaemon(boolean on):将线程设置为守护线程。守护线程在所有非守护线程结束后会自动退出。常用于后台任务。 -
setPriority(int newPriority):设置线程优先级,值范围是Thread.MIN_PRIORITY(1)到Thread.MAX_PRIORITY(10),默认值为Thread.NORM_PRIORITY(5)。优先级越高的线程更可能被优先调度。 -
getId():获取线程的唯一标识符 ID。 -
getName()和setName(String name):获取或设置线程的名称。可以帮助调试和日志输出。
2. Object 类中的线程相关方法
这些方法常用于线程的同步和通信,适用于 synchronized 块或方法中。
-
wait():使线程等待并释放锁,直到其他线程调用notify()或notifyAll()唤醒该线程。通常用于线程间的通信。 -
notify():唤醒一个正在等待的线程,如果有多个线程在等待,则随机选择一个唤醒。唤醒的线程会进入就绪状态,等待重新获取锁。 -
notifyAll():唤醒所有正在等待的线程,这些线程会进入就绪状态,但需要重新竞争锁。
为什么Java把wait()、notify() 和 notifyAll() 方法放在 Object 类中,而非 Thread 类
- 基于对象的锁机制:每个对象都可以充当锁,而不仅仅是线程实例,因此这些方法适用于所有对象。
- 线程间通信的通用性:线程通常通过共享对象来协作,而非直接互相控制,放在
Object类中更灵活。 - 面向对象设计:这种设计强调对象自带同步机制,符合封装性。
3.5 中断标记
中断标记是一个线程的状态标志,用来指示该线程是否被请求中断。在 Java 中,线程的中断标记是通过 Thread.interrupted() 或 Thread.currentThread().isInterrupted() 方法进行查询和设置的。它是一个布尔值,表示线程是否处于中断状态。
Thread.interrupted():检查并清除当前线程的中断标记。Thread.currentThread().isInterrupted():检查当前线程的中断标记,但不清除它。
3.6 哪些情况会清除中断标记
-
抛出
InterruptedException:
当线程在执行sleep()、wait()、join()等阻塞操作时被中断,抛出InterruptedException异常时,Java 会自动清除当前线程的中断标记。这是因为这些方法本身会通过异常机制响应中断,标记已被处理。 -
调用
Thread.interrupted():
调用Thread.interrupted()方法会检查并清除当前线程的中断标记。该方法返回当前线程的中断状态,并将中断标记清除。 -
执行中断相关的操作:
有些操作(如Thread.stop()和某些系统级的中断操作)可能会清除线程的中断标记,但这些操作不推荐使用,现代 Java 中主要使用Thread.interrupt()来请求线程中断。
3.7 为什么Java设计要清除中断标记
-
确保中断只处理一次:
当线程捕获InterruptedException异常时,清除中断标记表明该异常已经处理完毕。此时中断状态已经得到了响应,后续不再需要重复处理中断。 -
避免多次处理中断:
如果中断标记不清除,线程可能会多次无意义地响应相同的中断请求。清除标记可以确保线程处理一次中断后,状态变得清晰,避免多次重复响应。 -
线程控制权:
中断标记的清除使得线程有机会决定如何响应中断,而不是由 Java 底层强制处理。通过捕获InterruptedException,线程可以选择是否恢复中断标记或进行其他清理操作,从而给线程更多的控制权。 -
简化逻辑:
清除中断标记使得线程的中断状态变得明确,避免了中断标记在后续逻辑中的混乱。中断标记反映的是一个“事件”状态,清除它后,线程可以选择是否恢复这个状态(例如调用Thread.currentThread().interrupt())。
3.8 isInterrupted与interrupted
1. isInterrupted()
- 类型:实例方法,调用方式为
thread.isInterrupted() - 作用:检查调用它的线程对象的中断状态,并且不会清除中断标记
- 返回值:如果线程已被中断,返回true;否则返回false
2. interrupted()
- 类型:静态方法,调用方式为
Thread.interrupted() - 作用:检查当前线程的中断状态,并且在检查后清除中断标志(即将中断标志复位为
false)。 - 返回值:如果当前线程已被中断,返回
true,并清除中断标志;否则返回false
3.9 终止模式之两阶段终止模式
两阶段终止模式是一种常用的线程终止管理模式,旨在优雅地停止一个线程的执行。通常用于需要在结束前完成清理工作的场景,如定时任务、后台服务线程等。它通过两步(阶段)来实现线程的终止:
错误思路
- 使用线程对象的
stop方法:stop会真正杀死线程,若线程锁住了共享资源,则它被杀死后就再也没有机会释放锁 System.exit方法:目的是杀死一个线程,这种做法会让整个程序读停止
-
第一阶段:发出终止请求。
- 通过设置中断标记、发送信号等方法,通知目标线程准备终止。
- 线程在检测到终止请求后不会立即终止,而是进入一个清理阶段,以保证资源正确释放、数据完整。
-
第二阶段:安全终止线程。
- 在响应到终止请求后,线程进行必要的清理操作,如关闭文件、释放锁等,然后安全地退出。
- 此阶段保证线程在完成清理工作后才停止,避免资源泄露或数据损坏。
利用isInterrupted实现
public class Test {
private static Thread thread;
private static void start(){
thread = new Thread(() -> {
while(true){
if(thread.isInterrupted()){
log.info("料理后事...");
break;
}
try {
log.info("监控ing...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 中断标记会被清除
thread.interrupt(); // 手动恢复中断标记
}
}
}, "监控线程");
thread.start();
}
private static void stop(){
if(thread != null){
thread.interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
start();
Thread.sleep(3000);
stop();
}
}
3.10 start与run的区别
| 特性 | start() | run() |
|---|---|---|
| 作用 | 启动新线程并执行线程的任务 | 定义线程执行的具体任务 |
| 执行方式 | 启动新线程,异步执行 run() 方法 | 在当前线程执行 run() 方法 |
| 调用时机 | 调用后会触发线程的生命周期开始 | 直接调用时不会启动新线程 |
| 线程执行方式 | 新线程执行 run() 中的代码 | 当前线程执行 run() 中的代码 |
| 是否可以重复调用 | 只能调用一次,多次调用会抛出异常 | 可以多次调用,但不会启动新线程 |
| 使用场景 | 用于启动一个新线程,并发执行任务 | 用于定义线程的任务逻辑,或直接调用 |
3.11 sleep与yield
1. sleep
- 调用sleep会让当前线程从Running -> Timed Waiting状态(阻塞)
- 其他线程可以使用interrupt方法打断正在睡眠的线程,sleep方法会抛出
InterruptedException - 睡眠结束后线程未必会立即执行
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
2. yield
- 调用yield会让当前线程从Running -> Runnable就绪状态,然后调度执行其他线程
- 具体的实现依赖于操作系统的任务调度器
Thread.sleep() 和 Thread.yield() 都是 Java 中用于控制线程调度的方法,但它们的作用和行为有显著不同。以下是两者的对比:
| 特性 | Thread.sleep() | Thread.yield() |
|---|---|---|
| 功能 | 使当前线程暂停执行指定的时间(毫秒)。 | 提示当前线程让出 CPU 执行权,允许其他同优先级线程执行。 |
| 影响线程状态 | 当前线程进入 休眠 状态,暂停执行指定时间。 | 当前线程从 运行 状态变为 就绪 状态,继续等待 CPU 调度。 |
| 是否释放锁 | 是,线程在休眠时不会占用 CPU,也不会占用锁。 | 否,线程依然持有锁,其他线程无法获取锁。 |
| 作用范围 | 强制当前线程暂停指定时间,可以用于模拟延迟。 | 提示线程调度器让出 CPU 执行权,不保证立即暂停,适用于希望优化调度的场景。 |
| 是否一定发生 | 是,线程会在指定时间后恢复执行。 | 不一定会发生,调度器决定是否执行,让出 CPU 执行权。 |
| 使用场景 | 用于需要控制线程休眠一段时间(如定时任务、模拟延迟等)。 | 用于调度优化,允许同优先级线程更公平地轮流执行。 |
3.12 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
Thread daemon = new Thread(() -> {
try {
log.info("守护线程开始...");
Thread.sleep(5000);
log.info("守护线程结束...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "daemon");
daemon.setDaemon(true);
daemon.start();
Thread.sleep(500);
log.info("主线程结束...");
20:54:30.933 [daemon] INFO com.gor.concurrent.test.Test -- 守护线程开始...
20:54:31.436 [main] INFO com.gor.concurrent.test.Test -- 主线程结束...
3.13 变量的线程安全分析
1. 成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
2. 局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全的
// 成员变量线程不安全的例子
class Unsafe{
private final ArrayList<Integer> list = new ArrayList<>();
public void method1(int loopNumber){
for(int i = 0; i < loopNumber; i++){
method2();
method3();
}
}
private void method2(){
list.add(1);
}
private void method3(){
list.remove(0);
}
}
public class Test {
private final static int THREAD_NUMBER = 2;
private final static int LOOP_NUMBER = 1000;
public static void main(String[] args) throws InterruptedException {
Unsafe unsafe = new Unsafe();
for(int i = 0; i < THREAD_NUMBER; i++){
new Thread(() -> {
unsafe.method1(LOOP_NUMBER);
}).start();
}
}
}
// 局部变量线程安全的例子
// 加final限制method1方法不能被重写
// 加private保证method2、method3方法中的引用不会逃离方法的作用范围
class Safe {
public final void method1(int loopNumber){
ArrayList<Integer> list = new ArrayList<>();
for(int i = 0; i < loopNumber; i++){
method2(list);
method3(list);
}
}
private void method2(List<Integer> list){
list.add(1);
}
private void method3(List<Integer> list){
list.remove(0);
}
}
public class Test {
private final static int THREAD_NUMBER = 2;
private final static int LOOP_NUMBER = 1000;
public static void main(String[] args) throws InterruptedException {
Safe safe = new Safe();
for(int i = 0; i < THREAD_NUMBER; i++){
new Thread(() -> {
safe.method1(LOOP_NUMBER);
}).start();
}
}
}
// 把method3的修饰符改为public
class Safe {
public final void method1(int loopNumber){
ArrayList<Integer> list = new ArrayList<>();
for(int i = 0; i < loopNumber; i++){
method2(list);
method3(list);
}
}
private void method2(List<Integer> list){
list.add(1);
}
public void method3(List<Integer> list){
list.remove(0);
}
}
public class Test {
private final static int THREAD_NUMBER = 2;
private final static int LOOP_NUMBER = 1000;
public static void main(String[] args) throws InterruptedException {
Safe safe = new Safe(){
@Override
public void method3(List<Integer> list){
new Thread(() -> {
list.remove(0);
}).start();
}
};
for(int i = 0; i < THREAD_NUMBER; i++){
new Thread(() -> {
safe.method1(LOOP_NUMBER);
}).start();
}
}
}
从这个例子可以看出 private 或 final 提供【安全】的意义所在
3.14 常见的线程安全类
- 原子类:如 AtomicInteger、AtomicLong 等。
- 不可变类:如 String 和 Integer。
- 锁机制类:java.util.concurrent包下的类
注:
- 它们的每个方法是原子的
- 但多个方法的组合不是原子的,可能会有线程安全的问题
不可变类如何保证线程安全性?
public class Test {
public static void main(String[] args){
Integer i1 = 1000;
Integer i2 = i1;
i1 = 2000;
System.out.println(i1);
System.out.println(i2);
}
}
2000
1000 不可变类本身就不存在共享状态的变化,因此多线程环境下不会出现竞争条件
3.15 Monitor原理
Monitor - 监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
Monitor结构如下:
- 刚开始
Monitor中Owner为null - 当Thread-2执行
synchronized(obj)就会将Monitor的所有者Owner置为 Thread-2,Monitor中只能有一个Owner - 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
EntryList BLOCKED - Thread-2执行完同步代码块的内容,然后唤醒
EntryList中等待的线程来竞争锁,竞争时是非公平的 Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态BLOCKED和WAITING的线程都处于阻塞状态,不占用 CPU 时间片BLOCKED线程会在Owner线程释放锁时唤醒WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争
注意:
- synchronized必须是进入同一个对象的 monitor 才有上述的效果
- 不加synchronized 的对象不会关联监视器,不遵从以上规则
3.16 synchronized原理
从执行成本上看,持有锁是一个重量级的操作。Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统帮忙完成,这就不可避免地陷入用户态到核心态地转换中,进行这种状态转换需要耗费很多地处理器时间。有时状态转换时间甚至会比用户代码本身执行的时间还要长。
可以看出synchronized的局限性,在JDK5起Java类库提供了J.U.C包(java.util.concurrent),其中java.util.concurrent.locks.Lock接口便成为了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步。这也为日后扩展出不同的调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。
1. synchronized是否应该抛弃?
在JDK5及其之前,synchronized对性能的影响尤为显著;JDK6中加入了大量锁的优化措施,ReentrantLock和synchronized的性能基本持平。ReentrantLock在功能上是synchronized的超集,石否应该抛弃synchronized?
答:不应该抛弃。现在大多的程序都是使用JDK6或以上版本来部署的,所以可以忽略性能因素。
在ReentrantLock和synchronized都可满足需要时,优先使用synchronized:
- synchronized是语法层面的同步,足够清晰,也足够简单。每个程序员都熟悉synchronized,而J.U.C的Lock接口并非如此。
- Lock应该确保在finally块中释放锁,否则同步代码块中抛出异常,可能永远不会释放持有的锁。而synchronized可以由Java虚拟机确保即使出现异常,锁也能正确释放。
- 从长远看,Java虚拟机更容易针对synchronized进行优化,因为虚拟机可以在线程和对象的元数据中记录锁的相关信息,而使用Lock的话,虚拟机很难得知具体的锁对象是由哪些线程持有的。
2. 锁优化 - 自旋锁与自适应自旋锁
对性能影响最大的是阻塞的实现,挂起和恢复线程的操作都要转入内核态完成。现在绝大多数的电脑都是多核处理器,能让两个或以上的线程同时并行执行。我们可以让后面请求锁的线程等待一会,不放弃处理器的执行时间,看持有锁的线程是否很快会释放锁。为了让线程等待,只需让线程执行一个循环(自旋),这项技术就是自旋锁。
自旋锁在JDK1.4.2就已引入,但默认是关闭的,可以使用-XX:+UseSpinning参数来开启。JDK6就默认开启了。
如果锁被线程占用的时间很短,自旋等待的效果就非常好;反之如果锁被线程占用的时间很长,那么自旋的线程只会白白消耗处理器资源。因此有个自旋阈值,达到了阈值就会按传统方式挂起线程。默认是十次,可以使用-XX:PreBlockSpin来指定。
在JDK6中对自旋锁进行了优化,引入了自适应的自旋。自旋阈值不是固定的了,而是根据前一次在同一个锁上的自旋时间及锁的拥有者的状态决定的。如果在同一个锁对象上,自旋等待刚刚成功,那么虚拟机认为这次自旋等待也可能成功,进而增大阈值;反之,如果对于某个锁,自旋很少成功,在以后将有可能直接省略掉自旋过程。
3. 锁优化 - 锁消除
锁消除是指虚拟机即时编译器在运行时检测到某段需要同步的代码根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略。
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作线程私有的,同步加锁就无需执行了。
4. 锁优化 - 锁粗化
锁粗化的核心思想是:将多个相邻的、频繁加锁和解锁的操作合并为一个较大的锁范围。这样做的目的是减少锁的粒度,避免因为多次加锁/解锁带来的性能损失。
通常情况下,每次加锁和解锁都涉及到一些开销,尤其是在多线程环境下频繁的加锁解锁可能导致性能下降。通过粗化锁范围,可以减少加锁解锁的次数,从而提高性能。
public void example() {
synchronized (lock) {
// 操作 1
}
synchronized (lock) {
// 操作 2
}
synchronized (lock) {
// 操作 3
}
}
可能会粗化为
public void example() {
synchronized (lock) {
// 操作 1
// 操作 2
// 操作 3
}
}
5. 锁优化 - 轻量级锁
以32位虚拟机为例,HotShot虚拟机的对象头分为两部分,其中Mark Word用于存储对象自身的运行时数据,如哈希码、分代年龄。这部分是实现轻量级锁和偏向锁的关键。
轻量级锁工作流程:
-
在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象的Mark Word拷贝。
-
让锁记录的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
-
如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁。
-
如果CAS失败,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧。
是:当前对象已经拥有了这个对象的锁,发生了锁重入,新增一条锁记录作为重入的计数。
不是:表示出现多个线程竞争同一个锁的情况,轻量级锁不再有效,必须膨胀为重量级锁。
5. 解锁CAS成功,代表整个同步过程成功了;否则,说明有其他线程获取过该锁,在释放锁的同时,需要唤醒被挂起的线程。
对于绝大部分的锁,在整个同步周期内都是不存在竞争的。如果没有锁竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销。但如果存在竞争,除了本身的互斥量的开销外,还额外发生了CAS操作,此时轻量级锁反而比传统的重量级锁慢。
6. 锁优化 - 偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。
如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量;那么偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都没有了。以后只要不发生竞争,这个对象就归该线程所有。
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
在一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,Mark Word值为0x05即随后三位为101,这时thread、epoch、age都为0
- 偏向锁默认是延迟的,不会在程序启动后立即生效,可以使用参数
-XX:BiasedLockingStartupDelay=0来禁用延迟
偏向锁默认延迟生效是为了避免程序启动阶段因线程竞争频繁导致偏向锁的频繁撤销,从而增加性能开销。启动阶段通常有大量线程并发执行初始化任务,此时启用偏向锁可能适得其反。它有效避免了程序启动阶段频繁的锁竞争导致的偏向锁撤销开销,将偏向锁的优化作用集中于程序运行阶段,从而在实际运行中提升性能。
注意:!!!
Java HotSpot(TM) 64-Bit Server VM warning: Option BiasedLockingStartupDelay was deprecated in version 15.0 and will likely be removed in a future release.
Disable and Deprecate Biased Locking
JDK15起禁用和弃用偏向锁定
The biased locking is disabled by default and all related command-line options have been deprecated. See JEP 374: Disable and Deprecate Biased Locking. 默认情况下,偏向锁定处于禁用状态,并且所有相关的命令行选项均已弃用。请参阅JEP 374:禁用并弃用偏向锁定。
简言之就是偏向锁让代码更复杂、且效果甚微。
但是,我们还是需要掌握它的原理,毕竟目前JDK8和JDK11用的多。 这里选用JDK8进行演示
引入下面依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
// 提取jol工具分析出的Mark Word,并转换成二进制
public static void printMarkWordBinary(Object obj) {
// 获取对象头的内存布局
String layout = ClassLayout.parseInstance(obj).toPrintable();
// 从布局中提取 mark word 的 16 进制字符串
String markWordHex = extractMarkWord(layout);
// 将 16 进制字符串转换为二进制并打印
if (markWordHex != null) {
String binary = hexToBinary(markWordHex);
System.out.println("Mark Word (Binary): " + binary);
} else {
System.out.println("Mark Word not found.");
}
}
// 从打印的布局中提取 mark word 部分
private static String extractMarkWord(String layout) {
// 查找 "mark" 字段的行
String markWordPrefix = "(object header: mark)";
int startIdx = layout.indexOf(markWordPrefix);
if (startIdx != -1) {
int endIdx = layout.indexOf("\n", startIdx);
String markWordLine = layout.substring(startIdx, endIdx);
// 提取 "VALUE" 部分的第一个合法的 16 进制数
String[] parts = markWordLine.split(" ");
for (String part : parts) {
if (part.startsWith("0x")) {
return part.replace("0x", ""); // 去掉 0x 前缀
}
}
}
return null;
}
// 将 16 进制字符串转换为 2 进制字符串
private static String hexToBinary(String hex) {
try {
long value = Long.parseUnsignedLong(hex, 16);
return String.format("%064d", new java.math.BigInteger(Long.toBinaryString(value)));
} catch (NumberFormatException e) {
System.out.println("Failed to parse hex value: " + hex);
return null;
}
}
a. 测试延迟特性
public class TestBiasedLock {
public static void main(String[] args) throws InterruptedException {
printMarkWordBinary(new Object());
Thread.sleep(5000);
printMarkWordBinary(new Object());
}
}
b. 测试禁用延迟
添加参数-XX:BiasedLockingStartupDelay=0
c. 测试偏向锁
public class TestBiasedLock {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
printMarkWordBinary(obj);
synchronized (obj){
printMarkWordBinary(obj);
}
printMarkWordBinary(obj);
}
}
d. 撤销 - 调用hashcode方法
调用hashcode方法导致没地方存储线程id,此时偏向锁被撤销
public class TestBiasedLock {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
printMarkWordBinary(obj);
obj.hashCode();
printMarkWordBinary(obj);
}
}
e. 撤销 - 其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
// 因偏向锁和轻量级锁的前提都是没有竞争,所以这里加了别的锁保证t1执行完才执行t2
public class TestBiasedLock {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(() -> {
synchronized (obj){
printMarkWordBinary(obj);
}
synchronized (TestBiasedLock.class){
TestBiasedLock.class.notify();
}
}, "t1").start();
new Thread(() -> {
synchronized (TestBiasedLock.class){
try {
TestBiasedLock.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (obj){
printMarkWordBinary(obj);
}
}, "t2").start();
}
}
3.17 synchronization加锁流程
-
偏向锁(Biased Lock) :
- 初始情况下,对象的标志位为偏向锁。第一次有线程访问该对象时,JVM会将对象的Mark Word中的线程ID设置为当前线程的ID,表示该线程持有偏向锁。
- 如果同一线程再次访问该对象,直接进入同步块,无需进行额外的同步。
- 如果另一个线程尝试访问,且它的线程ID与Mark Word中的ID不同,则偏向锁会被撤销,转为轻量级锁。
-
轻量级锁(Lightweight Lock) :
- 当偏向锁被撤销后,JVM会尝试使用轻量级锁。线程会在栈帧中创建一个锁记录,并尝试通过CAS(Compare and Swap)操作交换Mark Word中的值,如果CAS成功,线程获得锁并执行同步代码块。
- 如果CAS失败,表示锁已被其他线程持有,当前线程会检查Mark Word中是否指向当前线程的栈帧,如果是,则重新进入同步块。
- 否则,锁会膨胀为重量级锁。
-
重量级锁(Heavyweight Lock) :
- 如果轻量级锁无法获得,JVM会将锁膨胀为重量级锁,创建一个Monitor对象,并将对象的Mark Word指向该Monitor。此时,线程进入Monitor的WaitSet,等待锁的释放。
- 获取锁的线程会在执行完同步代码后,通过唤醒WaitSet中的其他线程来通知它们获取锁。
-
锁的升级机制:
- 线程会经历从偏向锁到轻量级锁再到重量级锁的升级过程,且在特定情况下会撤销偏向锁或禁用偏向锁(如撤销次数达到一定阈值时)。
3.18 wait、notify、notifyAll
obj.wait()让进入Monitor的线程(Owner)到WaitSet等待obj.notify()在WaitSet中挑一个唤醒obj.notifyAll()唤醒WaitSet上的所有线程
这几个方法必须在同步环境中调用(即获得锁),以确保线程间的正确协调和数据一致性,避免错误的线程调度和虚假唤醒。
wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止wait(long n)有时限的等待, 到 n 毫秒后结束等待,或是被notify
3.19 sleep(long n)的wait(long n)区别
| 特性 | sleep(long n) | wait(long n) |
|---|---|---|
| 目的 | 暂停线程执行 | 线程等待某条件,释放锁 |
| 释放锁 | 不释放锁 | 释放当前持有的锁并进入等待队列 |
| 使用场景 | 控制延时,暂停线程 | 线程间协调(生产者-消费者等) |
| 是否与锁相关 | 与锁无关,只是线程暂停 | 与对象锁相关,常与 notify() 配合使用 |
| 抛出异常 | InterruptedException(线程中断) | InterruptedException(线程中断) |
3.20 同步模式之保护性暂停
Guarded Suspension,用在一个线程等待另一个线程的执行结果。在多线程程序中,为了避免多个线程同时访问共享资源,线程可以通过暂停来确保访问的顺序,从而避免冲突。
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) - JDK中,join的实现、Future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
1. 实现
public class GuardedObject {
private String response;
private final Object lock = new Object();
public Object get(){
synchronized (lock){
while(response == null){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}
public void executed(String response){
synchronized (lock){
this.response = response;
lock.notifyAll();
}
}
}
@Slf4j
public class Main {
public static void main(String[] args) {
GuardedObject go = new GuardedObject();
new Thread(() -> {
// 模拟t1在执行复杂任务
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
go.executed("我是t1线程,我结束了哦!");
}, "t1").start();
log.info("waiting...");
log.info("结果: {}", go.get());
}
}
2. 带超时版的GuardedObject
public String get(long millis){
synchronized (lock){
// 1) 最初的时间
long begin = System.currentTimeMillis();
// 2) 已经经历的时间
long timePassed = 0;
while(response == null){
long waitTime = millis - timePassed;
if(waitTime <= 0){
log.info("等待结束...");
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 3) 如果提前被唤醒,计算已经经历的时间
timePassed = System.currentTimeMillis() - begin;
}
return response;
}
}
@Slf4j
public class Main {
public static void main(String[] args) {
GuardedObject go = new GuardedObject();
new Thread(() -> {
// 模拟t1在执行复杂任务
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
go.executed("我是t1线程,我结束了哦!");
}, "t1").start();
log.info("waiting...");
log.info("结果: {}", go.get(1000));
}
}
3.21 异步模式之生产者/消费者
- 与前面的保护性暂停中的 GuardedObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列(例如
ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等),采用的就是这种模式
1. 实现
@Data
@AllArgsConstructor
class Message {
private int id;
private Object message;
}
@Slf4j
public class MessageQueue {
private final LinkedList<Message> queue = new LinkedList<>();
private int capacity;
public MessageQueue(int capacity){
this.capacity = capacity;
}
public Message take(){
synchronized (queue){
while(queue.isEmpty()){
log.info("消息队列为空,消费者等待...");
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Message msg = queue.removeFirst();
queue.notifyAll();
return msg;
}
}
public void put(Message msg){
synchronized (queue){
while(queue.size() == capacity){
log.info("消息队列已满,生产者等待...");
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.addLast(msg);
queue.notifyAll();
}
}
}
@Slf4j
public class Main {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for(int i = 0; i < 3; i++){
int id = i;
new Thread(() -> {
while (true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("生产消息...");
messageQueue.put(new Message(id, "生产者" + id + "的消息"));
}
}, "生产者" + id).start();
}
new Thread(() -> {
while(true){
Message msg = messageQueue.take();
log.info("消费消息: {}", msg.getMessage());
}
}, "消费者").start();
}
}