一. LockSupport
LockSupport 我们可以把它当成工具类来使用;
- park():阻塞挂起当前线程;
- unpark(Thread thread):恢复某个线程的运行;
值得一提的是,LockSupport 的 park() 不会抛出任何异常;
1. 常规使用
常规使用如下:
public class TestLockSupport {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("锁住当前线程t1");
LockSupport.park();
System.out.println("解锁了当前线程t1");
}
});
t1.start();
Thread.sleep(4000);
System.out.println("主线程解锁t1线程");
LockSupport.unpark(t1);
System.out.println(t1.isInterrupted());
}
}
打印如下:
锁住当前线程t1
主线程解锁t1线程
false
解锁了当前线程t1
2. park()
park() 可以被 LockSupport.unpark() 和 thread.interrupt() 打断;
2.1 LockSupport.unpark()
-
如果线程已经处于 park() 状态,即已经被阻塞,那么此时调用 unpark() 会使该线程解除阻塞状态;
-
如果线程尚未被阻塞,再次调用 unpark() 不会影响线程的执行状态,会确保线程下一次调用 park() 时立即返回,线程不会被阻塞;
多次调用 LockSupport.unpark() 有用吗?
LockSupport 的 park() 和 unpark() 是基于一个类似二元信号量的机制,即只有一个许可证可供使用,这个许可证是不可累积的,意味着无论 unpark() 被调用多少次,都只会释放一个许可证;
如果在一个线程尚未调用 park() 之前,多次调用 unpark(),那么只有第一次调用会实际产生效果,为后续可能的 park() 调用提供一个许可证,后续的 unpark() 调用在该线程下一次 park() 之前是没有实际作用的,因为它们不会累积许可证;
2.2 thread.interrupt()
- 对于被 park() 阻塞住的线程 thread,如果执行 thread.interrupt(),线程会被中断,线程中断标识位 thread.isInterrupt() == true,线程恢复运行;
- 可以理解为 LockSupport.unpark() 的效果和 thread.interrupt() 差不多,都会使线程 thread 重新运行,区别在于后者会将线程中断标识位 thread.isInterrupt() 置为 true;
示例如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("锁住当前线程t1");
LockSupport.park();
System.out.println("解锁了当前线程t1");
}
});
t1.start();
Thread.sleep(4000);
System.out.println("主线程解锁t1线程");
t1.interrupt();
System.out.println(t1.isInterrupted());
}
打印如下,可以看到的是,对于被 park() 阻塞住的线程,如果执行 thread.interrupt() 唤醒 thread 线程时,不会抛出异常;
锁住当前线程t1
主线程解锁t1线程
true
解锁了当前线程t1
二. Thread.sleep()
Thread.sleep() 是我们经常使用到的方法,但我们对这个方法的理解可能还不够,主要是因为 Thread.interrupt() 的存在;
1. 简单使用
主要使用如下,我们一般忽略 Thread.sleep() 抛出的 InterruptedException 异常;
public static void main(String[] args) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("当前线程被中断了");
e.printStackTrace();
}
System.out.println("主线程执行完成");
}
打印如下:
主线程执行完成
2. 执行thread.interrupt()
如果我们想打断正在 sleep() 的线程 t1,可以执行 t1.interrupt();
需要注意的是:
- 对于 LockSupport.park() 阻塞住的线程,执行 t1.interrupt() 会中断线程,t1 线程会恢复运行,t1.isInterrupted() == true;
- 对于 t1.sleep() 阻塞住的线程,执行 t1.interrupt() 会中断线程,t1.sleep() 会抛出 InterruptedException 异常,但是线程的中断标志位会重置为 false,t1.isInterrupted() == false;这里比较绕;
我们看一个 Thread.sleep() 被 interrupt() 中断的例子;
public class TestSleep02 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
System.out.println("thread working");
Thread.sleep(5000);
System.out.println("thread working over"); // 该行不会被执行
} catch (InterruptedException e) {
System.out.println("occur interruptException");
// 当 sleep() 被中断时,会抛出InterruptedException
System.out.println(Thread.currentThread().isInterrupted()); // false
// 通常我们也会在这里重新设置中断状态,以便上层代码知道发生了中断
// Thread.currentThread().interrupt();
}
});
thread.start();
Thread.sleep(3000);
System.out.println("开始打断 thread 线程");
thread.interrupt();
}
}
打印如下:
thread working
开始打断 thread 线程
occur interruptException
false
三. Thread.interrupt()
通过上面 LockSupport 和 Thread.sleep() 的学习,其实我们对 Thread.interrupt() 已经有了一定的了解,下面我们介绍一下 Thread.interrupt() 和线程的中断标志位;
-
interrupt() 会将线程的中断标志位 interrupt 置为 true,默认值是 false;
- 如果线程 t1 处于阻塞状态(即调用了sleep()、wait()、join()),当调用 t1.interrupt() 时会抛出中断异常InterruptedException,并且中断标记立即清除重置为 false;
- 如果线程 t1 是被 park() 阻塞挂起,当调用 t1.interrupt() 会打断 t1 线程,t1 线程恢复运行,t1 线程的中断标志位为 true,不会被清除重置;
-
isInterrupted() 会返回该线程的 interrupt 中断状态;
-
interrupted() 是测试当前线程是否处于中断状态,是的话返回 true,并且调用完毕后立即将该中断标记清除重置为 false;线程不是中断状态的话,直接返回 false;
需要注意一点,对于正常运行的线程来说,interrupt() 的作用只是将中断标志位置为 true,它并不是真正的中断线程;
1. interrupted()
对于 thread.interrupted(),它的作用是:测试当前线程是否处于中断状态,是的话返回 true,并且调用完毕后立即将该中断标记清除重置为 false;线程不是中断状态的话,直接返回 false;
我们举个例子:
public class TestInterrupt {
public static void main(String[] args) {
System.out.println("主线程开始");
Thread.currentThread().interrupt();
System.out.println("主线程是否处于中断状态:"+Thread.interrupted()); //true
System.out.println("主线程是否处于中断状态:"+Thread.interrupted()); //false
System.out.println("主线程是否处于中断状态:"+Thread.interrupted()); //false
System.out.println("主线程结束");
}
}
打印如下:
主线程开始
主线程是否处于中断状态:true
主线程是否处于中断状态:false
主线程是否处于中断状态:false
主线程结束
四. Monitor
monitor 是通过 C 来实现的,每个 Java 对象都有它关联的 Monitor 对象,结构如下:
ObjectMonitor() {
_count = 0; // 用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; // 锁的重入次数
_owner = NULL; // 指向持有 ObjectMonitor 对象的线程
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
}
- 首先,所有并发获取锁的线程会被放入 EntryList 里;
- 当一个线程获取到对象 A 的锁时,将 A 对象关联的 monitor 对象里的 owner 指向当前线程,将 monitor 里的 count 加1;
- 这时如果对象 A 调用 wait(),则将 monitor 中 count 减 1,将 owner 置为 null;并将该线程放入 waitSet 里,等待唤醒;
获取 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 中等待的线程来竞争锁;
-
WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足执行了 this.wait(),进入 WAITING 状态的线程;这些线程只有当被 notify() 时才会重新进入到 EntryList BLOCKED 竞争获取锁,获取锁成功的话从 wait() 处开始执行;
1. wait()和notify()
我们看下 Object.wait()、Object.notify()、Object.notifyAll() 的作用:
- Object.wait():当线程调用 wait() 时,它会释放对象的锁,并且放弃CPU,将自己置于该对象的等待队列中,等待被唤醒。
- Object.notify():当另一个线程拥有该对象的锁并调用 notify() 时,它会从等待队列中随机选择一个线程并唤醒它,使其有机会重新获取锁并继续执行;一旦 notify() 被调用,等待队列中的其他线程仍然处于等待状态,除非再次调用 notify() 或 notifyAll()。
- Object.notifyAll():与 notify() 不同,notifyAll() 会唤醒所有在该对象上等待的线程;一旦被唤醒,这些线程将竞争对象的锁,成功获取锁的线程将继续执行,而未获取锁的线程可能再次进入等待状态,直到锁被释放;
值得注意的是,wait()、notify() 和 notifyAll() 都必须在 synchronized 代码块或方法中调用,且调用的对象必须是synchronized 块或方法的锁对象。否则,将抛出 IllegalMonitorStateException 异常,因为这些方法的语义依赖于线程已经持有了对象的锁;
等待和阻塞不是一个概念,等待的线程是之前已经获取过锁的线程,当线程被唤醒后重新获取到锁时会从 obj.wait() 处运行代码;阻塞的线程是当前对象 obj 已经被其他线程持有了锁,于是只能阻塞挂起,直到锁被释放之后才会去争抢锁;
2. wait()和sleep()的区别
- sleep() 是 Thread 的静态方法,wait() 是 Object 的 方法;
- sleep() 可以在任意地方使用,wait() 和 notify() 只能在 synchronized 同步代码块中使用;
- sleep() 线程不会释放锁,它也不需要占有锁,wait() 线程会释放锁;