文章目录
一、interrupt 中断
关于中断的方法
Java中线程的中断(interrupt)是指线程的中断状态,true 或 false。Interrupt相关的方法:
isInterrupted :判断线程的中断状态,如果线程被中断返回true。
interrupt :中断当前线程,将中断状态设置为true。
interrupted :静态方法,判断当前线程是否中断,并且重置中断状态为false。
设置了线程中断状态为true后,线程会自己处理这个状态。
InterruptException
Thread.sleep()、Thread.wait() 等方法声明都会有 throws InterruptException,通常称这些方法为阻塞方法。阻塞方法通常需要较长的时间返回并且依赖其他外部条件,所以如果需要快速返回的话可以使用中断实现。
ReentrantLock 中 lock 方法处理中断:
AbstractQueuedSynchronizer#acquire | acquireQueued | parkAndCheckInterrupt | selfInterrupt
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果返回true表示在等待中被中断,调用方法再次设置中断状态为true
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果线程中断状态,设置interrupted变量为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
// 阻塞线程
LockSupport.park(this);
// 判断线程中断状态,并重新设置中断状态为false
return Thread.interrupted();
}
static void selfInterrupt() {
// 中断当前线程(设置中断状态true)
Thread.currentThread().interrupt();
}
ReentrantLock 中 lockInterruptibly 方法处理中断:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) throws InterruptedException {
// 如果线程状态为中断,则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
可以参考这两种方式处理线程中断,达到停止线程的目的。
自动感知中断
Thread类的 join 和 sleep 的重载方法,Object类的 wait 重载方法可以自动感知到线程的中断。这几个方法声明都有 throws InterruptException,当线程阻塞在这些方法上时,如果其他线程对这个线程进行中断,此线程会从这些方法立即返回,抛出异常,并且设置中断状态为false。
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
System.out.println("t1 acquire lock");
obj.wait(100000);
//LockSupport.park();
System.out.println("t1 finish park/wait");
} catch (Exception e) {
System.out.println("t1 state: " + Thread.currentThread().isInterrupted());
System.out.println("t1 InterruptedException");
}
}
});
t1.start();
Thread.sleep(100); // 先让线程t1获取到锁
boolean before = t1.isInterrupted();
t1.interrupt();
boolean after = t1.isInterrupted();
System.out.println("t1 interrupt before state: " + before);
System.out.println("t1 interrupt after state: " + after);
控制台输出(实际打印顺序每次都不一样):
t1 acquire lock
t1 state: false
t1 InterruptedException
t1 interrupt before state: false
t1 interrupt after state: true
线程t1被中断后线程的中断状态为true(并不是一定),wait方法返回,抛出异常。并且catch块中打印t1状态为false,说明中断状态又设置为false。如果在主线程中中断t1后过一段时间查看t1状态,那一定是false。
ReentrantLock 中 LockSupport.park(this) 将同步队列中线程阻塞,如果其他线程中断此线程,则线程会唤醒,但是中断状态还是true。
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
System.out.println("t1 acquire lock");
//obj.wait(1000);
LockSupport.park();
System.out.println("t1 state: " + Thread.currentThread().isInterrupted());
System.out.println("t1 finish park/wait");
} catch (Exception e) {
System.out.println("t1 InterruptedException");
}
}
});
t1.start();
Thread.sleep(100);
boolean before = t1.isInterrupted();
t1.interrupt();
boolean after = t1.isInterrupted();
System.out.println("t1 interrupt before state: " + before);
System.out.println("t1 interrupt after state: " + after);
控制台输出(实际打印顺序每次都不一样):
t1 acquire lock
t1 state: true
t1 finish park/wait
t1 interrupt before state: false
t1 interrupt after state: true
二、wait 等待
java中每个对象都关联一个 monitor,使用 synchronized 加锁,实际是获取对象关联的 monitor 锁的过程。同一时间只有一个线程获取到对象的monitor锁,如果其他线程在锁占用期间获取锁,将会阻塞在队列中(重量级锁)。
未获取到锁的线程被封装成ObjectWaiter添加到ContenttionList(cxq)中,持有锁的线程解锁前会将ContentionList中元素移动到EntiryList中。Owner表示当前持有锁的线程,加锁的对象调用wait方法将加入WaitSet(等待集合),当被唤醒或者等待时间到期后会重新加入到EntityList,重新参与锁的竞争。
wait 方法执行情况:
1 如果 线程t 未获取到 对象o 的锁,将抛出 IllegalMonitorStateException 异常
2 如果调用wait的重载方法,timeout不能为负数,nanos范围[0,999999],否则抛出IllegalArgumentException异常
3 如果线程t被中断,则抛出InterruptedException异常,并设置线程中断状态为false
4 线程加入等待集合,并完全释放对象o上的锁(发生重入)。等待直到从等待集合中移出,移出后参与锁竞争,继续完成加锁等操作。如果线程因为中断从等待集合中移出,中断状态将设置为false,并且抛出中断异常。
线程T由于主线程的打断,退出等待,与其他100个线程参与锁竞争:
CountDownLatch cdl = new CountDownLatch(100);
CountDownLatch cdl_0 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
System.out.println("t-T acquire lock");
obj.wait(10000);
} catch (Exception e) {
System.out.println("t-T InterruptedException");
}
System.out.println("t-T again acquire lock");
}
});
t1.start();
Thread.sleep(300);
IntStream.range(0, 100).forEach(i -> new Thread(() -> {
try {
cdl_0.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + " acquire lock");
}
cdl.countDown();
}, "t-" + i).start());
cdl_0.countDown();
t1.interrupt();
cdl.await();
System.out.println("end");
什么情况线程t会从等待集合(加锁对象对应monitor中的等待集合,是属于某个对象的)中移出,图中的d:
a 线程t被其他线程中断
b 其他线程中调用加锁对象的notify方法,有可能唤醒线程t(需要被选中)
c 其他线程中调用加锁对象的notifyAll方法,唤醒等待集合中所有线程,包括线程t
d 线程t调用wait有参数的重载方法,在指定的时间后从等待集合中移出
e JVM的“假唤醒”。
wait方法应该写在while循环中,等待某个条件成立后退出循环,避免假唤醒。
从等待集合中移出后的线程,需要等待当前线程释放锁之后,才会重新参与锁的竞争,竞争到锁才会继续执行(无论是中断抛出异常还是通知后正常执行,都需要再次获取到锁)。
三、notify、notifyAll 通知
notify、notifyAll 执行情况:
1 在 线程t 中 对象o 调用notify或者notifyAll方法后,如果线程t 还未 获取到 对象o 的锁,抛出IllegalMonitorStateException
2 notify 方法会从等待集合中选中一个线程(如果有)移出等待集合,这个线程将在t释放锁之后参与锁竞争
3 notifyAll 方法会将等待集合中所有线程移出,这些线程参与竞争
四、sleep、yield 休眠和让步
sleep方法使当前线程停止执行指令一段时间,与wait不同的是并不会释放已经获取的锁。
yield方法告诉当前系统的调度器让出cpu给其他线程用,调度器可以不理会。
五、线程的状态
一个线程在同一时间智能处于以下状态(虚拟机状态,和操作系统没有关系)之一:NEW、RUNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
下面是源码中的注释:
NEW:尚未启动的线程的线程状态。
RUNNABLE:可运行线程的线程状态。处于runnable 状态的线程正在Java虚拟机中执行,但是它可能在等待操作系统中的其他资源,比如处理器。
BLOCKED:等待监视器锁的阻塞线程的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法,或者在调用Object.wait后重新进入同步块/方法。
WAITING:等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:Object.wait \ Thread.join \ LockSupport.park。处于等待状态的线程正在等待另一个线程执行特定的操作。例如,一个在对象上调用object.wait()的线程正在等待另一个线程在该对象上调用object. notify()或object. notifyall()。调用thread.join()的线程正在等待指定的线程终止。
TIMED_WAITING:具有指定等待时间的等待线程的线程状态。线程由于调用下列方法之一,并指定了正等待时间,因此处于定时等待状态:Thread.sleep(long) \ Object.wait(long) \ Thread.join(long) \ LockSupport.parkNanos \ LockSupport.parkUntil 。
TERMINATED:终止线程的线程状态。线程已经完成执行。
结合注释我总结了线程状态之间的转换关系(20190911更新):
在 WAITING 和 TIMED_WAITING 状态时,其他线程执行等待超时、notify、notifyAll、interrupt 会变为 BLOCKED 状态,竞争到锁之后才是RUNNABLE状态。(结合monitor结构那张图就比较好理解了,waitSet集合中的线程会进入EntryList集合,参与下次锁竞争)
假设有5个线程去获取 obj 锁,然后 wait 100ms,可以查看 wait 超时之后每个线程的状态:
List<Thread> threadList = new ArrayList<>();
Thread thread;
for (int i = 0; i < 5; i++) {
thread = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
try {
System.out.println(threadName + " acquire lock");
obj.wait(100);
for (int j = 0; j < 5; j++) {
System.out.println(threadName + " -> "
+ threadList.get(j).getName() + " "
+ threadList.get(j).getState().name());
}
//obj.notifyAll();
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println(threadName +" InterruptedException");
}
}
}, "t-" + i);
threadList.add(thread);
}
for (int i = 0; i < 5; i++) {
threadList.get(i).start();
}
System.out.println("main thread end");
main thread end
t-0 acquire lock
t-3 acquire lock
t-2 acquire lock
t-4 acquire lock
t-1 acquire lock
t-2 -> t-0 TIMED_WAITING
t-2 -> t-1 BLOCKED
t-2 -> t-2 RUNNABLE
t-2 -> t-3 BLOCKED
t-2 -> t-4 BLOCKED
t-0 -> t-0 RUNNABLE
t-0 -> t-1 BLOCKED
t-0 -> t-2 RUNNABLE
t-0 -> t-3 BLOCKED
t-0 -> t-4 BLOCKED
t-3 -> t-0 TERMINATED
t-3 -> t-1 BLOCKED
t-3 -> t-2 TERMINATED
t-3 -> t-3 RUNNABLE
t-3 -> t-4 BLOCKED
t-1 -> t-0 TERMINATED
t-1 -> t-1 RUNNABLE
t-1 -> t-2 TERMINATED
t-1 -> t-3 TERMINATED
t-1 -> t-4 BLOCKED
t-4 -> t-0 TERMINATED
t-4 -> t-1 TERMINATED
t-4 -> t-2 TERMINATED
t-4 -> t-3 TERMINATED
t-4 -> t-4 RUNNABLE
可以看到每个线程依次获取到了锁(wait方法会释放锁),然后进入TIMED_WAITING状态,超时之后变为BLOCKED状态,RUNNABLE状态,执行完毕后是TERMINATED状态。这里可以倒推,最后一个执行的的线程t4,在t1执行时t4的状态是BLOCKED,同理t3执行时t1也是BLOCKED,符合图中内容。
这里可再验证一下中断的情况,让第一个等待超时并在此获取到锁的线程中断其他线程:
List<Thread> threadList = new ArrayList<>();
Thread thread;
for (int i = 0; i < 5; i++) {
thread = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
try {
System.out.println(threadName + " acquire lock");
obj.wait(100);
for (int j = 0; j < 5; j++) {
System.out.println(threadName + " -> "
+ threadList.get(j).getName() + " "
+ threadList.get(j).getState().name());
if (!threadName.equals(threadList.get(j).getName())) {
threadList.get(j).interrupt();
}
}
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println(threadName +" InterruptedException");
for (int j = 0; j < 5; j++) {
System.out.println(threadName + " -> "
+ threadList.get(j).getName() + " "
+ threadList.get(j).getState().name());
}
}
}
}, "t-" + i);
threadList.add(thread);
}
for (int i = 0; i < 5; i++) {
threadList.get(i).start();
}
System.out.println("main thread end");
main thread end
t-0 acquire lock
t-3 acquire lock
t-4 acquire lock
t-2 acquire lock
t-1 acquire lock
t-3 -> t-0 TIMED_WAITING
t-3 -> t-1 BLOCKED
t-3 -> t-2 BLOCKED
t-3 -> t-3 RUNNABLE
t-3 -> t-4 BLOCKED
t-4 InterruptedException
t-4 -> t-0 BLOCKED
t-4 -> t-1 BLOCKED
t-4 -> t-2 BLOCKED
t-4 -> t-3 TERMINATED
t-4 -> t-4 RUNNABLE
t-0 InterruptedException
t-0 -> t-0 RUNNABLE
t-0 -> t-1 BLOCKED
t-0 -> t-2 BLOCKED
t-0 -> t-3 TERMINATED
t-0 -> t-4 TERMINATED
t-1 InterruptedException
t-1 -> t-0 TERMINATED
t-1 -> t-1 RUNNABLE
t-1 -> t-2 BLOCKED
t-1 -> t-3 TERMINATED
t-1 -> t-4 TERMINATED
t-2 InterruptedException
t-2 -> t-0 TERMINATED
t-2 -> t-1 TERMINATED
t-2 -> t-2 RUNNABLE
t-2 -> t-3 TERMINATED
t-2 -> t-4 TERMINATED
可以看到t2抛出异常时是RUNNABLE状态,t1执行时t2是BLOCKED状态,也符合前面说的wait方法被中断线程从waitSet中移出,然后再次尝试获取锁,获取锁之后抛出异常。
验证线程执行sleep方法后是TIMED_WAITING状态,让5个线程竞争锁,然后让竞争到锁的线程休眠,同时在主线程中一直循环查看每个线程状态:
List<Thread> threadList = new ArrayList<>();
Thread t;
for (int i = 0; i < 5; i++) {
t = new Thread(()->{
synchronized (obj) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t-"+i);
threadList.add(t);
}
boolean b = true;
int a = 0;
while(b || a==0) {
for (int i = 0; i < 5; i++) {
Thread thread = threadList.get(i);
System.out.println(thread.getName() + " " + thread.getState().name());
if (i == 4 && thread.getState().equals(Thread.State.TERMINATED)) b=false;
}
if (a == 0) {
a++;
for (int i = 0; i < 5; i++) {
threadList.get(i).start();
}
}
}
控制台打印内容比较多,我截取了一部分:
t-0 NEW
t-1 NEW
t-2 NEW
t-3 NEW
t-4 NEW
t-0 RUNNABLE
t-1 RUNNABLE
t-2 RUNNABLE
t-3 RUNNABLE
t-4 RUNNABLE
t-0 TIMED_WAITING
t-1 BLOCKED
t-2 BLOCKED
t-3 RUNNABLE
t-4 RUNNABLE
t-0 TIMED_WAITING
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 BLOCKED
…
t-0 RUNNABLE
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 TIMED_WAITING
t-0 TERMINATED
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 TIMED_WAITING
…
六、join
join是Thread的一个实例方法,内部实现:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 测试此线程是否存活。如果一个线程已经启动并且尚未死亡,则该线程处于活动状态。
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
如果当前线程是活动状态,则调用当前线程的wait方法。也就是说导致线程wait返回的方式同样可以使线程join返回,例如notify、interrupt、等待超时等。join是同步方法,线程执行方法前后会自动执行加锁解锁操作,加锁对象为Thread的实例。
如果在线程t1中调用线程t2的join方法后,会发生什么?有点烧脑。
Thread t2 = new Thread(() -> {
System.out.println("t2 thread end");
}, "t2");
t2.start();
t2.join();
System.out.println("main thread end");
上面代码中在执行join方法前main线程(t1线程)和t2线程是并行的。执行join方法:
- 1 执行t2对象的同步方法,首先要获取到t2对象的锁。
- 2 获取到锁进入方法内部,循环(防止假唤醒)判断 t2线程 是否存活,t2如存活则调用wait方法,会将t1线程加入t2对象的等待集合中,同时释放锁。为什么判断的是t2是否存活,注释上说的并不是很清楚,首先isAlive是t2实例的方法,然后注释说明是“测试此线程是否存活。如果一个线程已经启动并且尚未死亡,则该线程处于活动状态。”如果这里代表t1那么t1一直满足这个存活条件,循环不会停止。
- 3 到wait为止,t2线程还在正常执行,t1已经被加入waitSet集合了也释放了锁,停止了执行。那么什么时候t1被唤醒?t2线程执行完毕后会调用notifyAll方法(join方法的注释上说明),唤醒等待集合中线程。
- 4 t1线程继续获取到锁,这时检测t2已经死亡,退出循环,join方法返回,t1继续执行。
直观的感觉就是t1线程在等待t2线程的执行,t2执行完毕后t1再从join方法后继续执行。
七、对比一下几个方法
方法 | Object | Thread |
---|---|---|
interrupt | 实例 | |
interrupted | 静态 | |
isInterrupted | 实例 | |
wait | 实例 | |
notify | 实例 | |
notifyAll | 实例 | |
sleep | 静态 | |
yield | 静态 | |
join | 实例 |
为什么wait、notify、notifyAll方法在Object中而不是Thread中?
- 这个问题还是要从synchronized关键字说起,synchronized同步代码块或者同步方法是对某个对象加锁,JVM中每个对象的对象头中markWord中关联了一个monitor,monitor中记录了重入次数、由于获取不到锁而阻塞的线程队列、当前用于锁的线程、由于调用wait方法而进入等待的线程集合等。处于等待状态的线程是不持有锁的,需要其他线程执行notify和notifyAll方法唤醒,线程从等待集合中移出,重新准备竞争锁。
- wait方法执行是需要释放锁的, 如果wait方法在Thread中,那么调用该方法时一定要记录下该线程是在执行同步方法(或者代码块)时进入了wait状态,因为其他线程执行notify或notifyAll唤醒这个线程后,需要重新获取这个同步方法的锁才能继续执行。另外还需要记录等待在方法上的线程,其他线程执行notify或notifyAll时需要通知这些线程去竞争锁。每个Thread对象都记录这些信息占用空间相比在Object对象占用的空间是比较大的。
- Java中可以对任意Object对象加锁,Object作为顶级父类,wait等方法适用每个对象。
- 另外在锁中记录那个线程正在持有锁,那些线程正在排队获取锁,那些线程在等待,要比在线程中记录获取了那个对象的锁,正在等待获取哪个对象的锁,要唤醒那些线程去竞争那些对象的锁要容易些。