一、Thread 类的属性及常用的构造方法
1.1、 Thread 常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) |
创建线程对象,并命名(名字是可以重复的); |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这 个目前我们了解即可 |
Ps:创建线程的时候,给线程起个名字还是很有必要的,目的就是为了方便程序员调试,一旦出问题,方便找到对应的代码,如果不手动起名,JVM 默认会起名为 thread-0、thread-1......
1.2、Thread 类的常见属性
获取方法 | 解释 |
getId() | 线程的 id,也是线程的唯一标识,不同线程不会重复 |
getName() | 名称,就是构造方法里给参数起的名字 |
getState() | 线程的状态(后面会细说) |
getPriority() | 优先级,优先级高的线程理论上更容易被调度到 |
isDaemon() | 是否是“后台线程”,这里需要记住一点——如果是“前台线程”,那么当 main 运行完了,前台线程还没完,进程是不会退出的!如果是后台线程,那么当 main 等其他前台线程运行完了,即使后台线程没执行完,进程也会退出! |
isAlive() | 判定内核线程还在不在,可以简单 理解为 run 方法执行完了,内核线程就销毁了。 |
isInterrupted() | 线程是否被中断 |
currentThread() | 返回当前线程对象的引用 |
1.3、启动(创建)一个线程
之前我们通过 new Thread 只是创建出了 Thread 对象,并没真正创建除线程!!!
Thread 对象虽然和内核中的线程是一一对应的,但是生命周期并非完全相同:Thread 对象创建出来了,内核中的线程还不一定有,调用 start 方法,内核的线程才创建出来;当 run 运行完了,内核中的线程就销毁了,但是 Thread 对象还在。
Ps:直接调用 run 并不会创建线程,只是运行线程中的代码,调用 start 方法,才是创建了线程
1.4、中断一个线程
当 run 方法执行完了,线程就销毁了,那有没有办法让线程提前结束呢?
使用 thread 的 interrupt 方法就通知这个线程进行中断,这个线程具体如何处理,还需要看 run 里的逻辑是如何实现的,主要有以下几种情况:
- thread 线程在运行状态,那么就设置标志 isInterrupted() 标志位为 true。
- thread 线程在阻塞状态(sleep),不会设置标志位,而是触发一个 InterruptedException 异常,这个异常会把 sleep 提前唤醒。
使用 interrupt 本质是让 run 方法尽快结束,而不是 run 执行一半强制结束!!!
最后,我们可以通过以下两种方法来看线程中断的标志位是否设置
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 判断当前线程的中断标志是否被设置,清除中断标志(相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位")
Thread.currentThread().isInterrupted() 判断指定线程的中断标志是否被设置,不清除中断标志 (相当于按下开关之后, 开关弹不起来, 这个称为 "不清除标志位".)
a)例如使用 Thread.isInterrupted() 判断线程是否收到中断通知后,标志位会被清除. 如下:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("当前线程是否收到中断的通知:" + Thread.interrupted());
}
}
});
thread.start();
thread.interrupt();
}
执行结果如下:
b) 使用 Thread.currentThread().isInterrupted() 判断线程是否收到中断通知后,标记位不会清除:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("当前线程是否收到中断的通知:" + Thread.currentThread().isInterrupted());
}
}
});
thread.start();
thread.interrupt();
}
1.5、等待一个线程
线程之间的调度顺序是完全随机的,但我们也可以通过一些特殊的操作来对线程的执行顺序进行干预,其中 join 就是一个办法~
在 main 方法中调用 t1.join 的效果就是让main 线程阻塞等待,等到 t1 线程执行完了,main线程才继续执行,如下代码:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("正在运行 t1 线程");
}
}
});
t1.start();
t1.join();
for(int i = 0; i < 5; i++) {
System.out.println("正在运行 main 线程");
}
}
执行效果:
如果去掉 t1.join(),效果如下:
另外,join 还有一个带等待时间的版本,如下:
也就是当超过了这个等待时间,就过时不候~
实际的开发中大多数都是指定了最大等待时间,避免程序“卡死”的情况~
1.6、休眠当前线程
sleep 指定休眠时间,可以让线程休息(阻塞)一会;
Thread 类下的静态方法 | 说明 |
public static void sleep(long millis) throws InterruptedException |
休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
Ps:因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
底层原理如下:
操作系统管理这些线程的 PCB 的时候,是有多个链表的,调用了sleep ,就会把 PCB 移动另外一个“阻塞队列”(原先在“就绪队列”),当 sleep 时间到了 ,就会被移动到之前的就绪队列。
Ps:移回了就绪队列,不代表立即就能在 CPU 上执行,还得看系统什么时候调用这个线程~
1.7、当前线程让出的 CPU 资源
通过 Thread.yield() 方法可以暂停当前正在执行的线程(让出当前的 CPU 资源),并执行其他线程。yield 做的就是让当前运行线程回到可运行的状态(就绪状态),以具有同样的优先级获得运行机会,也因此,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
Ps:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
二、线程状态
线程的状态是一个枚举类型 Thread.State。
不要被这个图吓到,他简化以后是这样的:
我们重点在于理解各个状态的意思:
- NEW: Thread 对象创建出来了,但是内核中的线程还没有创建。
- RUNNABLE: 就绪状态(正在 CPU 上运行 + 在就绪队列中排队)。
- WAITING: 特殊的阻塞状态,调用 wait。
- TIMED_WAITING: 按照一定时间,进行阻塞(sleep)。
- BLOCKED: 等待锁的时候进入阻塞状态。
- TERMINATED: 内核中的线程销毁了(线程的 run 方法执行完了),但是 Thread 对象还在。