Java 中提供了一些控制线程执行的便捷方法,以下介绍4种常用的线程控制方法。
1. join 线程
join 方法允许调用线程等待另一个线程完成后再运行。当调用线程在其本身的执行流中调用其他线程的 join 方法时,调用线程会进入阻塞状态,直到被调用 join 方法的线程执行完为止。
1.1 使用方式
join 方法一共有3种重载形式:
-
join()
:等待被调用 join() 方法的线程执行完为止。 -
join(long millis)
:等待被调用 join() 方法的线程最多执行 millis 毫秒,当过了指定时候后,如果被调用的线程还没有执行结束,也不会再等待。 -
join(long millis, int nanos)
:与第2种方式类似,只是最多等待的时间为 millis 毫秒 nanos 微秒。该形式不经常使用,主要是程序对时间的精度要求无需精确到微秒,并且,计算机软硬件本身也无法精确到微秒。
join 方法在调用 start 方法后使用,并且,调用该方法时会抛出 InterruptedException 异常,使用时要么捕获该异常要么显示声明抛出该异常。
1.2 使用场景
通常在需要将大问题分解成若干个小问题的场景下使用 join 方法,一般是在主线程中调用 join 方法,为每个小问题分配一个线程并调用子线程对象的 join 方法,等所有的小问题得到处理后,再由主线程完成进一步操作。
1.3 代码示例
public class JoinExample extends Thread {
public JoinExample(String name) {
super(name);
}
private int i = 0;
@Override
public void run() {
for (; i < 5; i++) {
System.out.println(this.getName() + ": " + i);
}
}
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName() + ": " + j);
if (j == 1) {
Thread joinExample1 = new JoinExample("被 Join 的线程1");
joinExample1.start();
Thread joinExample2 = new JoinExample("被 Join 的线程2");
joinExample2.start();
joinExample1.join();
joinExample2.join();
}
}
}
}
示例代码的打印结果为:
main: 0
main:1
被 Join 的线程1: 0
被 Join 的线程1:1
被 Join 的线程2: 0
被 Join 的线程2:1
被 Join 的线程2:2
被 Join 的线程2:3
被 Join 的线程1:2
被 Join 的线程1:3
被 Join 的线程1:4
被 Join 的线程2:4
main:2
main:3
main:4
从打印结果中看到,在主线程循环第2次时,启用了子线程并调用了子线程的 join 方法,并等待子线程遍历结束后,主线程才开始运行。但是,这两个子线程之间并没有固定的执行顺序。
2. 设置后台线程
在《Java多线程编程2:创建线程的3种方式》中介绍了线程可以分为2大类:用户线程(也称为“前台线程”)和守护线程。守护线程的任务是为其他的线程提供服务,也称为“精灵线程”。守护线程有一个很明显的特点:当所有的用户线程/前台线程都终止后,守护线程会自动终止。
setDaemon 方法可以将某个线程设置为守护线程。
2.1 使用方式
setDaemon 方法只有一种使用方式:setDaemon(boolean on)
,由程序来调用,设置某个线程是否为守护线程。setDaemon 方法是在 start 方法之前调用。
同时,Java 中也提供了一个 isDaemon 方法,判断某个线程是否为守护线程。
2.2 代码示例
public class SetDaemonExample extends Thread{
public SetDaemonExample(String name) {
super(name);
}
private int i = 0;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
}
}
public static void main(String[] args) {
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName() + ":" + j);
if (j == 1) {
Thread thread = new SetDaemonExample("设置的守护线程");
thread.setDaemon(true);
thread.start();
}
}
}
}
示例代码的运行结果:
main:0
main:1
main:2
main:3
main:4
设置的守护线程:0
设置的守护线程:1
设置的守护线程:2
设置的守护线程:3
设置的守护线程:4
设置的守护线程:5
从运行结果分析,虽然子线程计划循环100次,但当主线程循环结束后,主线程终止,因为子线程被设置为守护线程的原因,它也随之终止,因此,也只运行了5次。当调整主线程的循环上限时,子线程的循环结果也会随之增加。
3. 设置线程优先级
在 02 | 创建线程的3种方式中提到了每个线程都有一个优先级,新创建的子线程的默认优先级与创建它的父线程的优先级一致。优先级越高的线程就会获得较多的运行机会,而优先级低的线程获得较少的运行机会,这里需要注意的是,并不是先运行完高优先级的线程,才运行低优先级的线程。
setPriority 方法可以设置指定线程的优先级。
3.1 使用方式
setPriority 方法只有一种使用方式:setPriority(int newPriority)
,其中,newPriority 是一个整数,在 Java 中其范围为 0~ 10。但是,不同操作系统对于该范围的支持可能也不相同,例如,Windows 2000 仅提供了7个优先级。因此,通常使用 Thread 类提供3个静态常量来确保程序的可移植性。main 线程默认的优先级为 NORM_PRIORITY。
-
MAX_PRIORITY
:在 Java 中其值为10。 -
NORM_PRIORITY
:在 Java 中其值为5。 -
MIN_PRIORITY
:在 Java 中其值为1。
同时,Java 也提供了 getPriority 方法用来返回指定线程的优先级。
3.2 代码示例
public class SetPriorityExample extends Thread {
private int i;
public SetPriorityExample(String name) {
super(name);
}
@Override
public void run() {
for (; i < 10; i++) {
System.out.println(this.getName() + ":" + i);
}
}
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + ":" + j);
if (j == 1) {
Thread thread1 = new SetPriorityExample("高优先级线程");
thread1.start();
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new SetPriorityExample("低优先级线程");
thread2.start();
thread2.setPriority(Thread.MIN_PRIORITY);
}
}
}
}
4. 线程休眠
sleep 方法允许当前线程休眠一段时间,并进入阻塞状态。在线程的休眠时间内,它不会获得任何执行的机会,哪怕系统中没有其他可执行的线程。
Java 中也提供了一个类似 sleep 的方法:yield 方法。yield 方法可以短暂暂停当前运行的线程,但不会使得线程进入阻塞状态,而是进入就绪状态,然后让系统线程调度器重新调度一次,完全可能的情况是,一个线程调用 yield 方法短暂暂停之后,线程调度器又将其调度出来重新执行了。实际上,当某个线程中调用了 yield 方法后,只有优先级与其相同或者比其更高的处于就绪状态的线程才会获得执行的机会。
4.1 sleep 方法使用方式
与上面提到的3个方法不同,sleep 方法是 Thread 类的静态方法,有2种重载方式:
-
sleep(long millis)
:让当前正在运行的线程休眠 millis 毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响。 -
sleep(long millis, int nanos)
:让当前正在运行的线程休眠 millis 毫秒 nanos 微秒,但是该方法通常不使用。
4.2 yield 方法使用方式
yield 方法也是 Thread 类的静态方法,使用方式很简单:Thread.yield()
。
4.3 sleep 与 yield 的对比
-
调用 sleep 方法,当前线程会进入阻塞状态,只有休眠时间结束后,才会进入就绪状态;而调用 yield 方法,当前线程不会阻塞,而是进入就绪状态。
-
sleep 方法调用后,当前线程会进入阻塞状态,其他线程(无论优先级如何)获得运行的机会;而 yield 方法调用后,当前线程短暂暂停后,只有与其优先级相同或者比其优先级更高的线程才可以获得运行的机会。
-
调用 sleep 方法时会抛出 InterruptedException 异常,使用时要么捕获该异常要么显示声明抛出该异常;但调用 yield 方法时不会抛出任何异常。
-
sleep 方法比 yield 方法具有更好的移植性,通常不建议使用 yield 方法来控制并发线程的运行。
4.4 sleep 代码示例
public class SleepExample extends Thread {
private int i = 0;
public SleepExample(String name) {
super(name);
}
@Override
public void run() {
for (; i < 10; i++) {
System.out.println(this.getName() + ":" + i);
if (i == 5) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + ":" + j);
if (j == 1) {
Thread thread = new SleepExample("被休眠的线程");
thread.start();
}
if (j == 2) {
Thread.yield();
}
}
}
}