线程的优先级
简介
现代操作系统基本采用分时的形式调度运行线程,操作系统会分出一个个的时间片,线程会分配到若干时间片,当线程的时间片用完之后就会发生线程调度,并等待下次分配。线程分配到的时间片的多少决定了线程使用处理器资源的多少,线程优先级就是决定线程需要分配多少处理器资源的线程属性。
在Java中,通过一个整型变量priority来控制优先级,优先级的范围为 1~10,默认的优先级是5:
在JDK中使用三个常量定义了三个优先级的值
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
如果优先级小于1大于10,则抛出:
throw new IllegalArgumentExceptin()
优先级高的线程分配时间片的数量要远多于优先级低的线程。设置线程优先级时,针对频繁阻塞的线程(休眠或者I/O操作)需要设置较高优先级,而偏重计算(需要消耗较多CPU时间)的线程则设置较低的优先级,确保处理器不会被独占。
API
// 设置线程优先级
setPriority(int newPriority)
// 得到线程的优先级
getPriority()
注意
-
高优先级的线程总是大部分先执行完,但不代表高优先级的线程都执行完
-
并非先被main线程调用就会先执行完
-
当线程的优先级等级差距很大时,谁先执行与调用顺序无关
结论: 程序正确性不能依赖线程的优先级高低,因为操作系统可以完全不理会Java线程对优先级的设定。线程的优先级具有一定的随机性。
线程的状态
Java线程在运行的生命周期中有6中状态:
| 状态名称 | 描述 |
|---|---|
| NEW | 初始状态,线程被构建,但还没有调用start()方法 |
| RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为运行中 |
| BLOCKED | 阻塞状态,表示线程在锁上阻塞 |
| WAITING | 等待状态,表示线程进入等待状态,当前线程需要等待其他线程做出动作(通知或中断) |
| TIME_WAITING | 超时等待状态,可以在指定时间内返回 |
| TERMINATED | 终止状态,表示该线程已经执行完毕 |
Java线程的状态变迁图如下:

分析上图可得:
-
线程调用start()方法开始执行
-
当执行wait()方法后,线程进入等待队列进行等待(需要其他线程通知才能返回)
-
超时等待状态线程在等待时间内返回,返回后直接进入运行态
-
线程调用同步方法时(synchronizzed),且没有获取到锁,则进入同步队列进行阻塞等待(阻塞状态)
-
线程在执行Runnable的run()方法后进入终止状态
Daemon 线程
Java分两种线程:用户线程和守护线程。守护线程是一种支持型线程,被用作程序后台调度以及支持性的工作,最典型的守护线程如:垃圾回收线程。当Java虚拟机中不存在非Daemon线程的时候,JVM将会退出。
守护线程和用户线程没有本质区别,Java中可以使用 Thread.setDaemon(true) 将用户线程设置为守护线程。所谓守护,其实要有守护的线程才能称为守护,所以当JVM中没有用户线程时,也就无所谓守护了,自然JVM也就会杀死守护线程而退出。
使用守护线程时需要注意:
-
Thread.setDaemon(true) 必须在 Thread.start() 之前设置,不能把正常运行的线程设置为守护线程,否则会抛出 java.lang.IllegalThreadStateException
-
在守护线程中产生的线程也是守护线程
-
守护线程应该永远不去访问固有资源,如:文件,数据库,因为它会在任何时候甚至一个操作的中间发生中断
API 的使用如下:
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
// 设置为守护线程
thread.setDaemon(true);
// 启动线程
thread.start();
// 是否是守护线程
System.out.println(thread.isDaemon());
}
}
关于中断
什么是中断
对于已经启动或正在运行的线程,有时候我们需要取消该线程的操作,但要使线程快速,安全的停止并非易事,于是Java提供了一种中断操作:能够使一个线程终止另一个线程当前的工作。
线程的中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者在可能的情况下停止当前工作,并转而执行其他工作。
合适时刻(取消点): wait(), sleep(), join()。这些可阻塞的方法都是通过提前返回或者抛出 InterruptedException 来响应中断
何时中断
-
线程处于阻塞状态:Thread.sleep(), Object.wait()
当线程调用中断方法时,将执行操作:清除中断状态,抛出 InterruptedException,表示阻塞操作由于中断而提前结束
-
线程处于非阻塞状态(正常运行)
当线程调用中断方法时,中断状态将被设置,但线程不会立即停止正在执行的工作,而是传递请求中断的消息,然后在合适的取消点中断自己
API
// 线程调用此方法设置中断状态
public void interrupt()
// 静态方法,返回当前线程的中断状态
// 清除当前线程的中断状态,并返回之前的值
public static boolean interrupted()
// 线程调用此方法,返回目标目标线程的中断状态
// 如果当前线程已经中断过了,返回 true;
// 如果在调用该方法前调用了 Thread.interrupted(),对线程中断标识进行了复位,则返回false
public boolean isInterrupted()
安全的终止线程
suspend(),resume()和stop()能够很灵活对线程进行暂停,唤醒和终止操作,但由于其带来的副作用太大:会占有资源进入睡眠状态,很容易引发死锁问题,所以已经不推荐使用了。
现在介绍一种更为优雅安全的终止线程的方法:通过中断和标识位来安全的终止线程,如下面的例子:
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runner(), "ThreadOne");
thread.start();
// 睡眠一秒, main线程对 thread 进行中断,使 thread 感知到中断而结束
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
Runner two = new Runner();
thread = new Thread(two,"ThreadTwo");
thread.start();
// 睡眠一秒, main线程对 two线程 进行取消,使 thread 感知到on为false结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("count i = " + i);
}
public void cancel() {
on = false;
}
}
}
// 结果如下:
count i = 212612516
count i = 238490750
分析程序可得:main线程使用中断和 cancel() 方法均可使线程安全的终止。这种通过标识位或者中断操作的方式能使线程在终止时有机会去清理资源,而不是直接终止,因此会更加安全优雅。
线程间通信
假如线程之间没有任何交流,只是各做各的是,那么多线程的意义不大。多线程的美妙之处在于线程之间信息的传递,即线程间的通信。由于线程之间的信息传递,使得多线程编程功能更加强大灵活。
在具体谈线程之间如何通信之前,先谈谈线程之间的同步问题。我们知道,为了使多线程之间能够同步的访问共享资源,Java使用Synchronized关键字来保证同步。本质上就是:为每一个需要被共享的资源加锁(synchronized方法或synchronized代码块),锁就是对象监视器(类对象或者类实例),在多线程访问共享资源之前,需要先获取对象监视器(锁),这个获取过程是互斥的,在同一时刻只能有一个线程获取到由 synchronized 保护的对象监视器,其他线程则在同步队列等待获取。下面是关系图:

如上图可知,任意线程对Object的访问,首先需要获得Object的监视器。线程获取监视器失败则进入同步队列,线程状态变为阻塞状态。当前一个线程释放了锁,则释放操作唤醒阻塞在同步队列中的线程,使该线程重新去获取锁。
等待/通知机制
等待/通知机制是每一个Java对象都具备的能力,这些方法都被定义在了 java.lang.Object 中
| 方法 | 描述 |
|---|---|
| wait() | 调用该方法的线程进入等待状态,只有等待其他线程的通知或被中断才会返回。调用wait()后,释放对象的锁 |
| wait(long) | 超时等待 |
| wait(long, int) | 时间精度更高的超时等待 |
| notify() | 通知一个对象上等待的线程,使其从wait()方法返回,返回的前提就是线程获取了对象的锁 |
| notifyAll() | 通知所有等待在该对象上的线程 |
等待/通知的运行示意图如下:

-
使用wait(),notify(),notifyAll()时需要先对调用对象加锁
-
调用wait()后,线程由RUNNING变为WAITING,并将当前线程放入到对象的等待队列中
-
notify()和notifyAll()方法调用后,等待线程不会马上从wait()返回,需要调用notify()和notifyAll()的线程释放锁之后,等待线程才有机会返回
-
notify()方法将等待队列中的一个线程移入同步队列,notifyAll()方法则将等待队列中的所有线程移入同步队列,被移动的线程由WAITING变为BLOCKED
-
线程从wait()方法返回的前提是获得了调用对象的锁
等待/通知的经典模型
1. 生产者/消费者模型
消费者模型
- 获取对象的锁
- 如果条件不满足,调用对象的wait()方法,被通知后检查条件
- 条件满足则执行对应逻辑
synchronized(object) {
while(条件不满足) {
object.wait()
}
// 逻辑处理
}
生产者模型
- 获得对象的锁
- 改变条件
- 通知所有等待在对象上的线程
synchronized(object) {
// 改变条件
object.notifyAll()
}
2. 等待/超时模型
生产者/消费者模型通过:加锁、条件循环和逻辑处理3个步骤,完美的实现了线程之间的通信,但这种模式却无法做到超时等待的场景:线程A在Object的等待队列等待T时间,并在T时间内自动返回。
等待超时模型如下:
// 对当前对象加锁
public synchronized Object get(long mills) throws InterruptedException {
// 超时返回的时刻
long future = System.currentTimeMills() + mills;
// 超时时间
long remaining = mills;
// 当返回结果为空且没有达到超时时间时等待
while ((result == null) && ramining > 0) {
wait(remaining);
remaining = future - System.currentTimeMills();
}
return result;
}
等待/超时模型在等待/通知的基础上增加了超时控制,使得该模式更具有灵活性。即使方法执行时间过长,也不会永久阻塞调用者,而是会按照调用者要求的时间返回。
参考
《Java并发编程的艺术》
《Java并发编程实战》

本文详细探讨了Java线程的优先级、状态、中断机制、守护线程、线程间通信及等待/通知机制,提供了丰富的代码示例,帮助读者理解多线程编程的核心概念。

被折叠的 条评论
为什么被折叠?



