发布时间:2024-09-04 11:01
尾部-----阿里10万字java面试手册我已打包成PDF。完整资料文章尾端 点赞+转发+关注。
LockSupport 是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。
与 Thread.suspend() 方法相比,它弥补了由于 resume() 方法发生导致线程无法继续执行的情况。LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数,而仅仅两个简单的接口,就为上层提供了强大的同步原语。
和 Object.wait() 方法相比,它不需要先获得某个对象的锁,也不会抛出 InterruptedException 异常
如果你阅读 JDK 有关 Thread 类的 API 文档,可能还会发现两个看起来非常有用的接口,即线程挂起( suspend )和继续执行( resume )。
这两个操作是一对相反的操作,被挂起的线程,必须要等到 resume 方法操作后,才能继续指定。
乍看之下,这对操作就像 Thread.stop() 方法一样\"好用\"。
但如果你仔细阅读文档说明,会发现它们也早已被标注为废弃方法,并不推荐使用。
不推荐使用 suspend 方法去挂起线程是因为 suspend 方法在导致线程暂停的同时,并不会释放任何锁资源。
此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行(如图所示)。
直到对应的线程上进行了 resume 方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。
但是,如果 resume 方法操作意外地在 suspend 方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。
并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。
而且,对于被挂起的线程,从它的线程状态上看,居然还是 Runnable ,这也会严重影响我们对系统当前状态的判断。
/**
* 挂起此线程。
*
* 首先,调用此线程的checkAccess方法时不带参数。这可能导致抛出SecurityException(在当前线程中)。
*
* 如果线程处于活跃状态,它将被挂起,并且在恢复之前不会取得进一步的进展。
*
* @exception SecurityException–如果当前线程无法修改此线程。
*
* @deprecated 这个方法已经被弃用了,因为它天生就容易死锁。
* 如果目标线程被挂起的时候在监视器上持有一个锁,用来保护关键的系统资源,则在目标线程恢复之前,任何线程都不能访问该资源。
* 如果要恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。
* 这种死锁通常表现为“冻结”进程。
*/
@Deprecated
public final void suspend() {
checkAccess();
suspend0();
}
private native void suspend0();
/**
* 恢复挂起的线程。
*
* 首先,调用此线程的checkAccess方法时不带参数。这可能导致抛出SecurityException(在当前线程中)。
*
* 如果线程处于活动状态,但处于挂起状态,则会恢复该线程,并允许其在执行过程中取得进展。
*
* @exception SecurityException–如果当前线程无法修改此线程。
*
* @deprecated 此方法只存在于与suspend一起使用时,suspend已被弃用,因为它容易死锁。
*/
@Deprecated
public final void resume() {
checkAccess();
resume0();
}
private native void resume0();
为了支持多线程之间的协作,JDK 提供了两个非常重要的接口:线程等待 wait() 方法和通知 notify() 方法。
为什么这两个方法并不是在 Thread 类中的,而是在 Object 类里面?
一方面意味着任何对象都可以调用这两个方法。
另一方面, 无论是 wait() 方法或者 notify() 方法都需要首先获得目标对象的一个监视器(Monitor)。
如果定义在 Thread 类里面,是不方便获取目标对象的 Monitor 的;定义在 Object 类里面,只需要 synchronized(this) 就能获取当前对象的 Monitor。
为什么需要监视器(Monitor)?
之所以需要监视器(Monitor),本质上是确保同一时间只会有一个线程操作同一个物理区域(JVM内存中的Object),同时它也保障了不同线程之间的有效通信。
当在一个对象实例上调用 wait() 方法后,当前线程就会在这个对象上等待。
这是什么意思呢?
比如,在线程 A 中,调用了 obj.wait() 方法,那么线程 A 就会停止继续执行,转为等待状态。
等待到何时结束呢?
线程 A 会一直等到其他线程调用了 obj.notify() 或者 obj.notifyAll() 方法为止。
这时,object 对象俨然成了多个线程之间的有效通信手段。
wait() 方法和 notify() 方法究竟是如何工作的呢?
如果一个线程调用了 object.wait() 方法,那么它就会进入 object 对象的等待队列(wait set)。
这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。
当 object.notify() 方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。
这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。
这里还需要强调一点,object.wait() 方法并不能随便调用。
它必须包含在对应的 synchronized 语句中,无论是 wait() 方法或者 notify() 方法都需要首先获得目标对象的一个监视器。
下图显示了 wait() 方法和 notify() 方法的工作流程细节。
其中 T1 和 T2 表示两个线程。
T1 在正确执行 wait() 方法前,必须获得 object 对象的监视器。
而 wait() 方法在执行后,会释放这个监视器。
这样做的目的是使其他等待在 object 对象上的线程不至于因为 T1 的休眠而全部无法正常执行。
线程 T2 在 notify() 方法调用前,也必须获得 object 对象的监视器。
所幸,此时 T1 已经释放了这个监视器。因此,T2 可以顺利获得 object 对象的监视器。
接着,T2执行了 notify() 方法尝试唤醒一个等待线程,这里假设唤醒了 T1。
T1 在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得 object 对象的监视器,而这个监视器也正是 T1 在 wait() 方法执行前所持有的那个。
如果暂时无法获得,则 T1 还必须等待这个监视器。当监视器顺利获得后,T1 才可以在真正意义上继续执行。
Object#wait
**
* 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法。
*
* 换句话说,这个方法的行为就像它只是执行调用 wait(0) 一样。
*
* 当前线程必须拥有此对象的监视器。
*
* 线程释放此监视器的所有权并等待,直到另一个线程通过调用 notify 方法或 notifyAll 方法通知等待此对象监视器的线程唤醒。
*
* 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。
*
* 在单参数版本中,中断和虚假唤醒是可能的,并且此方法应始终在循环中使用:
*
*
* synchronized (obj) {
* while (条件不满足的时候)
* obj.wait(timeout, nanos);
* ... // 执行适合条件的操作
* }
*
*
* 此方法只能由作为此对象监视器所有者的线程调用。
*
* 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
* @throws InterruptedException 如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
*/
public final void wait() throws InterruptedException {
wait(0);
}
/**
* 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法,或者经过指定的时间。
*
* 当前线程必须拥有此对象的监视器。
*
* 此方法导致当前线程(称为 T )将自身置于此对象的 wait set 中,然后放弃此对象上的所有同步声明。
*
* 线程 T 因线程调度目的而被禁用,并处于休眠状态,直到发生以下四种情况之一:
*
* 1. 另一个线程调用这个对象的 notify 方法,而线程T恰好被任意选择为要唤醒的线程。
* 2. 其他一些线程为此对象调用 notifyAll 方法。
* 3. 其他线程会中断线程 T。
* 4. 指定的真实时间已过去,或多或少。但是,如果超时为 0,则不考虑实时性,线程只是等待通知。
*
* 然后从该对象的 wait set 中删除线程 T,并重新启用线程调度。
*
* 然后,它以通常的方式与其他线程竞争在对象上同步的权利;
*
* 一旦它获得了对对象的控制权,它对对象的所有同步声明都将恢复到原来的状态,也就是说,恢复到调用 wait 方法时的状态。
*
* 然后,线程 T 从 wait 方法的调用返回。
*
* 因此,从 wait 方法返回时,对象和线程 T 的同步状态与调用 wait 方法时的状态完全相同。
*
* 线程也可以在没有收到通知、中断或超时的情况下唤醒,这就是所谓的虚假唤醒。
*
* 虽然这种情况在实践中很少发生,但应用程序必须通过测试本应导致线程被唤醒的条件来防范这种情况,如果条件不满足,则继续等待。
*
* 换句话说,等待应该总是在循环中发生,比如:
*
*
* synchronized (obj) {
* while (条件不满足的时候)
* obj.wait(timeout, nanos);
* ... // 执行适合条件的操作
* }
*
*
* 如果当前线程在等待之前或等待期间被任何线程中断,则抛出 InterruptedException。
*
* 在如上所述还原此对象的锁定状态之前,不会引发此异常。
*
* 注意,wait 方法在将当前线程放入这个对象的 wait set 中时,只解锁这个对象;当线程等待时,可以同步当前线程的任何其他对象将保持锁定状态。
*
* 此方法只能由作为此对象监视器所有者的线程调用。
*
* 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
*
* @param timeout 以毫秒为单位的最长等待时间。
* @throws IllegalArgumentException 如果超时值为负数或nanos的值不在0-999999范围内。
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
* @throws InterruptedException 如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
*/
public final native void wait(long timeout) throws InterruptedException;
/**
* 使当前线程等待,直到另一个线程为此对象调用 notify() 方法或notifyAll() 方法,或者其他线程中断当前线程,或者经过一定的实际时间。
*
* 此方法类似于一个参数的 wait 方法,但它允许更好地控制在放弃之前等待通知的时间量。
*
* 实时量(以纳秒为单位)由下式给出: 1000000 * timeout + nanos
*
* 在所有其他方面,此方法的作用与一个参数的方法 wait(long) 相同。
*
* 特别地,wait(0,0) 与 wait(0) 的意思相同。
*
* 当前线程必须拥有此对象的监视器。
*
* 线程释放此监视器的所有权,并等待发生以下两种情况之一:
*
* 1. 另一个线程通过调用 notify 方法或 notifyAll 方法通知等待此对象监视器的线程唤醒。
* 2. 超时时间(由超时毫秒数加纳秒参数指定)已过。
*
* 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。
*
* 在单参数版本中,中断和虚假唤醒是可能的,并且此方法应始终在循环中使用:
*
*
* synchronized (obj) {
* while (条件不满足的时候)
* obj.wait(timeout, nanos);
* ... // 执行适合条件的操作
* }
*
*
* 此方法只能由作为此对象监视器所有者的线程调用。
*
* 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
*
* @param timeout 以毫秒为单位的最长等待时间。
* @param nanos 附加时间,纳秒范围0-999999。
* @throws IllegalArgumentException 如果超时值为负数或nanos的值不在0-999999范围内。
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
* @throws InterruptedException 如果任何线程在当前线程等待通知之前或期间中断了当前线程。引发此异常时,将清除当前线程的中断状态。
*/
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException(\"timeout value is negative\");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
\"nanosecond timeout value out of range\");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
/**
* 唤醒正在等待此对象监视器的单个线程。
*
* 如果有任何线程正在等待这个对象,其中一个线程被选择唤醒。
*
* 这种选择是任意的,由 实现类 自行决定。
*
* 线程通过调用其中一个 wait 方法来等待对象的监视器。
*
* 在当前线程放弃对该对象的锁定之前,唤醒的线程将无法继续。
*
* 唤醒的线程将以常规方式与任何其他线程竞争,这些线程可能会积极地在这个对象上进行同步竞争;例如,唤醒的线程在成为下一个锁定此对象的线程时并没有优劣可言。
*
* 此方法只能由作为此对象监视器所有者的线程调用。
*
* 线程通过以下三种方式之一成为对象监视器的所有者:
*
* 1. 通过执行该对象的同步实例方法。
* 2. 通过执行在对象上同步的synchronized语句的主体。
* 3. 对于类类型的对象,执行该类的同步静态方法。
*
* 一次只能有一个线程拥有对象的监视器。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
*/
public final native void notify();
/**
* 唤醒等待此对象监视器的所有线程。
*
* 线程通过调用其中一个 wait 方法来等待对象的监视器。
*
* 在当前线程放弃对该对象的锁定之前,唤醒的线程将无法继续。
*
* 唤醒的线程将以常规方式与任何其他线程竞争,这些线程可能会积极地在这个对象上进行同步竞争;例如,唤醒的线程在成为下一个锁定此对象的线程时并没有优劣可言。
*
* 此方法只能由作为此对象监视器所有者的线程调用。
*
* 请参阅 notify 方法,以了解线程成为监视器所有者的方式的描述。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
*/
public final native void notifyAll();
线程的状态在java中有明确的定义,在java.lang.Thread.State中有6种。
① NEW
线程被创建,未执行和运行的时候
② RUNNABLE
不代表线程在跑,两种:被cpu执行的线程,随时可以被cpu执行的状态。
③ BLOCKED
线程阻塞,处于synchronized同步代码块或方法中被阻塞。
④ WAITING
等待线程的线程状态。线程当前不执行,如果被其他唤醒后会继续执行的状态。依赖另一个线程的通知的。这个等待是一直等,没人叫你,你起不来。
⑤ TIMED_WAITING
指定等待时间的等待线程的线程状态。带超时的方式:Thread.sleep,Object.wait,Thread.join,LockSupport.parkNanos,LockSupport.parkUntil
⑥ TERMINATED
正常执行完毕或者出现异常终止的线程状态。
线程状态流转图
从NEW状态出发后,线程不能再回到NEW状态,同理,处于 TERMIINATED 状态的线程也不能再回到 RUNNABLE状态。
/**
* 线程状态。线程可以处于以下状态之一:
* NEW:尚未启动的新线程处于此状态。
* RUNNABLE:在Java虚拟机中执行的线程处于此状态。
* BLOCKED:阻塞等待监视器锁定而被阻塞的线程处于此状态。
* WAITING:正在无限期等待另一个线程执行特定操作的线程处于此状态。
* TIMED_WAITING:TIMED_WAITING正在等待另一个线程执行某个操作达指定等待时间的线程处于此状态。
* TERMINATED:终止已退出的线程处于此状态。
*
* 线程在给定的时间点只能处于一种状态。
* 这些状态是不反映任何操作系统线程状态的虚拟机状态。
*
* @since 1.5
*/
public enum State {
/**
* 尚未启动的线程的线程状态。
*/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,如处理器。
*/
RUNNABLE,
/**
* 阻塞等待一个监视器锁时的线程状态。
* 为了进入同步块/方法处于阻塞状态的线程会等待监视器锁,或在调用{@link Object#wait() Object.wait}后重新进入同步块/方法。
*/
BLOCKED,
/**
* 等待线程的线程状态。
* 由于调用以下方法之一,线程处于等待状态:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
* 处于等待状态的线程正在等待另一个线程执行特定操作。
*
* 例如,对某个对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()或Object.notifyAll()。
*
* 调用Thread.join()的线程正在等待指定的线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。
* 由于使用指定的正等待时间调用以下方法之一,线程处于定时等待状态:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
*/
TIMED_WAITING,
/**
* 终止线程的线程状态。线程已完成执行。
*/
TERMINATED;
}
为了更好的将 LockSupport 和 suspend 进行对比,这里我们举一个例子。
suspend
下面的代码中 suspend 方法将导致线程卡死。
package com.shockang.study.java.concurrent.thread.suspend;
public class SuspendDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread(\"t1\");
static ChangeObjectThread t2 = new ChangeObjectThread(\"t2\");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println(\"in \"+getName());
Thread.currentThread().suspend();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}
现在用 LockSupport 重写这个程序:
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
private static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread(\"t1\");
static ChangeObjectThread t2 = new ChangeObjectThread(\"t2\");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println(\"in \" + getName());
LockSupport.park();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
注意,这里只是将原来的 suspend 方法和 resume() 方法用 park() 方法和 unpark() 方法做了替换。
当然,我们依然无法保证 unpark() 方法发生在 park() 方法之后。
但是执行这段代码,你会发现,它自始至终都可以正常地结束,不会因为 park() 方法而导致线程水久挂起。
这是因为 LockSupport 类使用类似信号量的机制。
它为每一个线程准备了一个许可,如果许可可用,那么 park() 方法会立即返回,并且消费这个许可(也就是将许可变为不可用)。
如果许可不可用,就会阻塞,而 unpack 方法则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。
这个特点使得:即使 unpack() 方法操作发生在 park() 方法之前,它也可以使下一次的 park() 方法操作立即返回。
这也就是上述代码可顺利结束的主要原因。
线程状态 WAITING (parking)
同时,处于 parko 方法挂起状态的线程不会像 suspend 方法那样还给出一个令人费解的 Runnable 状态。
它会非常明确地给出一个 WAITING 状态,甚至还会标注是 park() 方法引起的。
这使得分析问题时格外方便。
\"t1\" #8 prio=5 os_prio=0 tid=0x00b1a400 nid=0x1994 waiting on condition [0x1619f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util..concurrent.locks..LockSupport.park(LockSupport.java: 304)
at com.shockang.study.java.concurrent.lock.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java: 19)
- locked <0x048b2680> (a java.lang.Object)
此外,如果你使用 park(Object)函数,那么还可以为当前线程设置一个阻塞对象。
这个阻塞对象会出现在线程 Dump 中。
这样在分析问题时,就更加方便了。
比如,我们将上述代码第 14 行的 park() 方法改为
LockSupport.park(this);
那么在线程 Dump 时,你可能会看到如下信息:
\"t1\" #8 prio=5 os_prio=0 tid=0x0117ac00 nid=0x2034 waiting on condition [0x15d0f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x048b4738> (a com.shockang.study.java.concurrent.lock.LockSupportDemo$ChangeObjectThrea)
at java.util..concurrent.locks..LockSupport.park(LockSupport.java: 304)
at com.shockang.study.java.concurrent.lock.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java: 19)
- locked <0x048b2808> (a java.lang.Object)
注意,在堆栈中,我们甚至还看到了当前线程等待的对象,这里就是 ChangeObjectThread 实例
支持中断
除了有定时阻塞的功能, LockSupport.park() 方法还能支持中断影响。
但是和其他接收中断的函数很不一样, LockSupport.park() 方法不会抛出 InterruptedException 异常。
它只会默默返回,但是我们可以从 Thread.interrupted() 等方法中获得中断标记。
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.LockSupport;
public class LockSupportIntDemo {
private static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread(\"t1\");
static ChangeObjectThread t2 = new ChangeObjectThread(\"t2\");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println(\"in \" + getName());
LockSupport.park();
if (Thread.interrupted()) {
System.out.println(getName() + \" 被中断了\");
}
}
System.out.println(getName() + \"执行结束\");
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}
in t1
t1 被中断了
t1执行结束
in t2
t2执行结束
这套阿里10万字java面试手册我已打包成PDF。需要完整资料的朋友 点赞+转发+关注。私信回复暗号【444】即可免费获取!