Java线程<第二篇>:线程API详细介绍
(1)休眠 sleep
sleep 是一个静态方法,它有两个重要的重载,分别是:
public static native void sleep(long millis);
public static void sleep(long millis, int nanos);
第一个重载方法是本地方法,形参是一个以毫秒为单位的时间整数;
第二个重载方法源码中有具体实现:
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
它有两个形参,分别是以毫秒为单位的时间,以及以纳秒为单位的时间整数。
从源码中分析,当满足 nanos >= 500000
或者 nanos != 0 && millis == 0
条件之后,毫秒数自增1,最终依然调用了 sleep 的本地方法。
下面是具体使用方法:
Thread.sleep(3000); // 当前线程休眠 3000 毫秒
Thread.sleep(3000, 500000); // 当前线程休眠 3001 毫秒
Thread.sleep(3000, 800000); // 当前线程休眠 3001 毫秒
Thread.sleep(3000, 400000); // 当前线程休眠 3000 毫秒
Thread.sleep(0, 400000); // 当前线程休眠 3001 毫秒
Thread.sleep(0, 800000); // 当前线程休眠 3001 毫秒
从逻辑上,具体的休眠时间根据源码可以计算出,但是,需要知道的是,这个时间精度要以系统的定时器和调度器的精度为准。
(2)TimeUnit 替代 Thread.sleep
实际上,我们几乎不会使用 Thread.sleep,TimeUnit 同样可以实现一样的效果:
代码的具体使用如下:
try {
TimeUnit.HOURS.sleep(3); // 时
TimeUnit.MINUTES.sleep(3); // 分
TimeUnit.SECONDS.sleep(3); // 秒
TimeUnit.MILLISECONDS.sleep(3000); // 毫秒
TimeUnit.MICROSECONDS.sleep(3001); // 微妙
TimeUnit.NANOSECONDS.sleep(3001); // 纳秒
} catch (InterruptedException e) {
e.printStackTrace();
}
TimeUnit 可以直接指定时间单位,比 Thread.sleep 的可读性更高,源码如下:
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}
从源码上分析,TimeUnit 本质上使用的是 Thread.sleep, 两者对比,TimeUnit 的优点如下:
(1)可读性更高:TimeUnit 指定了时间单位,传入的时间参数更加易懂;
(2)扩展性更好:TimeUnit 可以指定的时间单位有:日、时、分、秒、微妙、纳秒;
(3)线程 yield
yield 方法属于启发式的方法,其会提醒调度器我愿意放弃当前的 CPU 资源,如果 CPU 的资源不紧张,则会忽略这种提醒。
调用 yield 方法会使当前线程从 RUNNING 状态切换到 RUNNABLE 状态,一般这个方法不太常用。
先看下以下代码:
private static Thread createThread(int index) {
return new Thread(new Runnable() {
@Override
public void run() {
if (index == 1) {
System.out.println("放弃当前CUP资源");
Thread.yield();
}
System.out.println(index);
}
});
}
public static void main(String[] args) {
for (int index = 1; index < 3; index++) {
createThread(index).start();
}
}
假设CPU是单核CPU,那么CPU的调度结果见下图:
单核CPU,使用yield调度流程.jpg(4)线程优先级
thread.setPriority(7);
threadGroup.setMaxPriority(10);
一般情况下,不会对线程设定优先级,更不会让某些业务严重地依赖线程的优先级别。
(5)获取线程ID
thread.getId();
调用 getId 可以获取线程唯一 ID,线程的 ID 在整个 JVM进程中都会是唯一的,并且是从 0 开始逐次递增。
(6)获取当前线程
Thread.currentThread();
(7)线程上下文类加载器
thread.getContextClassLoader();
获取线程上下文加载器,简单来说就是这个线程是由哪个类加载器加载的,如果是在没有修改线程上下文加载器的情况下,则保持与父线程同样的类加载器。
thread.setContextClassLoader(ClassLoader classLoader);
设置该线程的类加载器,这个方法可以打破 JAVA 类加载器的父委托机制,有时候该方法也被称为 JAVA 类加载器的后门。
(8)线程 interrupt
interrupt 是中断的意思,当线程进入阻塞状态时,执行 interrupt 会让线程退出阻塞状态,interrupt 被称之为中断方法;
能够让线程阻塞的方法有:
(1)Object 的 wait 方法;
(2)Thread 的 sleep 方法;
(3)Thread 的 join 方法;
(4)io 操作;
(5)其它方法;
一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterrupttedException 异常,这个异常就像一个 signal(信号)一样通知当前线程被打断了。
演示代码如下:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread线程开始");
try {
TimeUnit.MINUTES.sleep(1); // 休眠 1 分钟
} catch (InterruptedException e) {
System.out.println("捕获了InterruptedException异常");
}
System.out.println("thread线程结束");
}
});
thread.start();
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
thread.interrupt(); // 中断
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
System.out.println("main线程结束");
}
执行结果如下:
thread线程开始
捕获了InterruptedException异常
thread线程结束
main线程结束
(9)线程 isInterrupted
isInterrupted 是 Thread 的一个成员方法,注意和 Thread 的静态方法 interrupted 之间的区别。
isInterrupted 方法是判断线程是否是中断状态,下面举两个例子:
举例一:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
System.out.println("是否是中断状态1:" + thread.isInterrupted());
thread.interrupt(); // 中断
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
System.out.println("是否是中断状态2:" + thread.isInterrupted());
}
在线程中,执行了一个无限执行的 while 语句,当执行到 thread.interrupt() 时,interrupter 标识不会被擦除,因此,在执行 thread.interrupt() 之前中断状态是 false,在执行 thread.interrupt() 之后中断状态是 true。
是否是中断状态1:false
是否是中断状态2:true
虽然中断状态被置为 true,但是不代表线程被中断,实际上,线程中的 while 仍然在执行。
一般情况下,interrupt 方法需要和阻塞方法一起使用,比如下面一段代码实现:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.MINUTES.sleep(1); // 休眠 1 分钟
} catch (InterruptedException e) {
System.out.println("捕获了InterruptedException异常");
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
System.out.println("是否是中断状态1:" + thread.isInterrupted());
thread.interrupt(); // 中断
TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
System.out.println("是否是中断状态2:" + thread.isInterrupted());
}
当执行 thread.interrupt() 后,InterruptedException 异常被捕获,与此同时,interrupt 标志被擦除,输出结果是:
是否是中断状态1:false
捕获了InterruptedException异常
是否是中断状态2:false
执行 thread.interrupt() 之后,isInterrupted 的返回值依然是 false,原因是:线程的阻塞方法被中断之后,interrupt 标志会被擦除。
(10)静态方法 interrupted
interrupted 是一个静态方法,它的作用是获取是否是 interrupt 状态,它的作用和 thread.interrupt() 是一样的,但是它们却有本质的区别:
(1)thread.interrupt():只有使用阻塞方法并捕获到 InterruptedException 异常之后才会擦除 interrupt 状态;如果没有阻塞方法,那么 interrupt 状态不会被擦除;
(2)Thread.interrupted():Thread.interrupted() 第一次执行返回的是真实的 interrupt 状态,执行之后 interrupt 状态被擦除;当第二次执行 Thread.interrupted() 后, interrupt 状态已经被擦除,所以第二次执行的返回值是false;
(10)线程 join
join 和 sleep 一样,都是阻塞方法,都可以捕获到中断信号,并且擦除线程的 interrupt 标识,它有3个重载方法,分别是:
thread.join();
thread.join(3000);
thread.join(3000, 1);
演示代码如下:
/**
* 创建一个线程,名称为 index
* @param index
* @return
*/
private static Thread create(int index) {
return new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 " +index + " 开始");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " +index + " 结束");
}
}, String.valueOf(index));
}
public static void main(String[] args) throws InterruptedException {
System.out.println("线程 main " + "开始");
for (int index = 1; index < 3; index++) {
Thread thread = create(index);
thread.start();
thread.join();
}
System.out.println("线程 main " + "结束");
}
在 main 方法(main 线程)中,创建了两个线程,先 start,再 join,join 方法会使 main 方法暂时阻塞;
名称为 1 的线程是首先 join,名称为 2 的线程是第二个 join,这两个线程的执行是顺序的(串行的),当执行完名称为 1 的线程之后才会执行名称为 2 的线程;
当执行 join方法 的线程全部执行完毕之后,才继续执行 main 方法;
以上代码的打印结果如下:
线程 main 开始
线程 1 开始
线程 1 结束
线程 2 开始
线程 2 结束
线程 main 结束
join 方法还可以传递具体的时间,比如,将 thread.join() 改成 thread.join(3000);
即线程1执行3秒之后,开始执行线程2,但是线程1会继续执行;
线程2执行3秒之后,main 线程 和 线程 1 都继续执行;
最终的打印结果如下:
线程 main 开始
线程 1 开始
线程 2 开始
线程 1 结束
线程 main 结束
线程 2 结束
(11)如何关闭一个线程
JDK有一个被弃用的 stop 方法,该方法在关闭线程的时候可能不会释放掉 monitor 的锁,这里不建议使用该方法结束线程。
那么,应该如何关闭呢?
(1)等待线程正常结束,完成线程的生命周期;
(2)如果含有阻塞方法,那么可以捕获中断异常,执行线程的 interrupt 方法;
(3)使用一个变量控制是否需要停止线程的开关,为了在多线程的情况下线程安全,那么需要添加volatile关键字,代码如下:
public class MyThread extends Thread {
private volatile boolean isClosed = false;
@Override
public void run() {
while (!isClosed && !isInterrupted()) {
}
System.out.println("线程执行结束");
}
/**
* 关闭线程
*/
public void closed () {
isClosed = true; // 关闭标识
interrupt(); // 中断
}
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
TimeUnit.SECONDS.sleep(5);
myThread.closed(); // 关闭线程
}
(4)程序异常退出
(5)进程假死
有时候JVM的任务比较繁重,其它任务的执行被阻塞,导致进程假死的可能。
[本章完]