线程控制与线程通信基础

记录一下线程控制和线程通信的 API 等。

线程控制

线程控制的主要方法在 Thread 类中。

开始线程

开始一个线程使用线程的 start() 方法。

暂停线程

暂停线程意味着线程还可以重新恢复,Java 提供了几种暂停的方法:sleep()join()yield()wait()notify() 组合。

sleep(long millis)

public static native void sleep(long millis) throws InterruptedException

使当前正在执行的线程睡眠,不释放锁权限(monitor),睡眠结束后进入就绪态。

join()、join(long millis)

public final synchronized void join(long millis) throws InterruptedException

JDK 8 中给出的解释是 Waits for this thread to die.
举个栗子会更好理解:比如在主线程(main)中调用某个线程 t 的 join() 的方法,主线程会一直等待线程 t 结束才开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}
}

就拿刚才的例子结合源代码来理解。
join() 方法是一个同步方法,因此需要获取锁对象,在 main 线程中调用线程 t 的 join() 方法,即 main 线程获取到了 t 线程对象的内在锁(同步方法的锁对象即调用者,此处即 t 线程对象),在一个 while 循环中反复判断 t 线程是否结束,如果 t 线程还没结束,则调用 this.wait() 方法,使持有 t 线程对象锁的 main 线程处于等待状态,直到 t 线程结束,join 方法结束返回,此时会调用 this.notifyAll()

1
2
3
4
5
6
7
8
9
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++)
System.out.println("i=" + i);
});

thread.start();
thread.join();

System.out.println("main");

yield()

public static native void yield()

暂停当前线程。对调度程序暗示当前线程可以暂时停止让出 CPU 占用,但是要取决于 CPU 的调度,CPU 可能会忽略这个暗示。

线程中断

由于 Java 无法立即停止一个线程,而终止操作很重要,所以 Java 提供了一种用于停止线程的机制,即中断机制。

  • 中断状态
    每个线程维护一个中断状态位,用来表明该线程是否被中断,默认为非中断 false。
  • 中断方法
    中断方法仅仅只能设置和检测中断状态。
  • 中断过程
    JDK 只负责检测和更新中断状态,因此中断的处理过程需要开发者自己实现,包括中断捕获和处理。

中断方法

  • public static boolean interrupted()
    判断当前线程是否中断,静态方法,会清除中断状态(设置为 false)。
  • public boolean isInterrupted()
    判断线程是否中断,对象方法,不会清除中断状态。
  • public void interrupt()
    中断操作,对象方法,会将线程的中断状态设置为 true,仅此而已。

InterruptedException

一个方法声明中抛出 InterruptedException 异常,说明该方法执行可能需要花费一些时间等待,该方法可以使用 Thread 实例的 interrupt() 方法来中断。

能够抛出 InterruptedException 异常的方法:

  • java.lang.Object 的 wait() 方法(需要其他线程通过 notify() 唤醒,可能会一直等待)
  • java.lang.Thread 的 sleep(millis) 方法(等待设定的时间)
  • java.lang.Thread 的 join() 方法(等待指定线程结束)

只有在线程执行到这些能够抛出 InterruptedException 异常的方法时,线程调用 interrupt() 方法,这些方法才会中断并执行 catch 块中代码。其中调用了 wait() 方法的线程在使用 interrupt() 方法时需要重新获取锁对象再抛出 InterruptedException 异常,而 sleep(millis) 方法和 join() 方法不需要重新获取锁对象,能够立马执行 catch 块代码。

中断处理

几种常用的中断处理。

  • 异常法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Thread thread = new Thread(() -> {
    try {
    if(Thread.currentThread().isInterrupted()) {
    throw new InterruptedException();
    }
    } catch (InterruptedException e) {
    System.out.println("成功捕获中断异常");
    e.printStackTrace();
    }
    });
    thread.start();
    thread.interrupt();
  • 阻塞法
    有时候抛出 InterruptedException 并不合适,例如当由 Runnable 定义的任务调用一个可中断的方法时,就是如此。在这种情况下,不能重新抛出 InterruptedException,但是你也不想什么都不做。当一个阻塞方法检测到中断并抛出 InterruptedException 时,它会清除中断状态。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Thread thread = new Thread(() -> {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    System.out.println("成功捕获中断异常");
    // 重新设置中断状态
    Thread.currentThread().interrupt();
    }
    });
    thread.start();
    thread.interrupt();
  • return 法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Thread thread = new Thread(() -> {
    while (true) {
    if (Thread.currentThread().isInterrupted()) {
    System.out.println("线程停止");
    return;
    }
    System.out.println(System.currentTimeMillis());
    }
    });
    thread.start();
    Thread.sleep(1000);
    thread.interrupt();

设置守护线程

public final void setDaemon(boolean on)

设置某个线程为守护线程,守护线程(后台线程)需要等待所有前台线程结束后才会结束(所有前台线程结束后,无论后台线程是否处在运行中,都会被强制结束)。例如:OS 会为 JVM 创建一个进程,JVM 会启动一个前台线程来运行 main 方法和一个后台线程来运行 GC。

在使用时,只要在创建线程后调用该方法,传入 true 即可将该线程设置为守护线程。

1
2
3
4
5
6
7
8
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++)
System.out.println("i=" + i);
});
thread.setDaemon(true);
thread.start();

System.out.println("main");

循环打印可能等不到全部打印就结束了,因为 main 线程只打印了一句,很快执行完后线程结束,此时守护线程也会被强制结束。

线程通信基础

wait()notify()notifyAll() 这些方法常用来进行线程间通信,它们必须在同步代码块或同步方法中使用。

wait()

public final native void wait() throws InterruptedException

调用 wait() 方法后,持有对象锁的线程释放对象锁权限,释放 CPU 执行时间并进入等待状态,等待其他线程唤醒。

wait(long timeout)

public final native void wait(long timeout) throws InterruptedException

在超时时间内,可以被其他线程使用 notify()notifyAll() 来唤醒,或者在超时后会由系统自动唤醒。如果超时时间为 0,等同于 wait() 方法。

notify()

public final native void notify()

持有对象锁的线程释放对象锁权限,通知 jvm 唤醒某个竞争该对象锁的线程。其他竞争线程继续等待(即使该同步结束,释放对象锁,其他竞争线程仍然等待,直至有新的 notify() 或 notifyAll() 被调用)。

notifyAll()

public final native void notifyAll()

持有对象锁的线程释放对象锁权限,通知 jvm 唤醒所有竞争该对象锁的线程,jvm 通过算法将对象锁交给某个线程,所有被唤醒的线程不再等待,而是一起竞争该对象锁权限。

参考

处理 InterruptedException