1.线程Sleep

sleep是一个静态方法, 其有两个重载方法, 其中一个需要传人毫秒数, 另外一个既需要毫秒数也需要纳秒数。

1.1 sleep方法介绍

  1. public static void sleep(long millis) throws InterruptedException
  2. public static void sleep(long millis int nanos) throws InterruptedException

sleep方法会使当前线程进入指定毫秒数的休眠, 暂停执行, 虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃monitor锁的所有权。

1.2 使用Time Unit替代Thread.sleep

  1. Thread.sleep(12257088L)
  2. Time Unit.HOURS.sleep(3)
  3. Time Unit.MINUTES.sleep(24)
  4. Time Unit.SECONDS.sleep(17)
  5. Time Unit.MILLISECONDS.sleep(88)

同样的时间表达, Time Unit显然清晰很多, 笔者强烈建议, 在使用Thread.sleep的地方, 完全使用Time Unit来代替, 因为sleep能做的事, Time Unit全部都能完成, 并且功能更加的强大, 在本书后面的内容中, 我将全部采用Time Unit替代sleep。

2.线程yield

2.1 yield方法介绍

yield方法属于一种启发式的方法, 其会提醒调度器我愿意放弃当前的CPU资源, 如果CPU的资源不紧张, 则会忽略这种提醒。调用yield方法会使当前线程从RUNNING状态切换到RUNNABLE状态, 一般这个方法不太常用:

2.2 yield和sleep

  • sleep会导致当前线程暂停指定的时间, 没有CPU时间片的消耗。
  • yield只是对CPU调度器的一个提示, 如果CPU调度器没有忽略这个提示, 它会导致线程上下文的切换。
  • sleep会使线程短暂block, 会在给定的时间内释放CPU资源。
  • yield会使RUNNING状态的Thread进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话)。
  • sleep几乎百分之百地完成了给定时间的休眠, 而yield的提示并不能一定担保。
  • 一个线程sleep另一个线程调用interrupt会捕获到中断信号, 而yield则不会。

3.设置线程的优先级

  1. public final void setPriority(int new Priority) 为线程设定优先级。
  2. public final int getPriority() 获取线程的优先级。

3.1 线程优先级介绍

  • 对于root用户, 它会hint操作系统你想要设置的优先级别, 否则它会被忽略。
  • 如果CPU比较忙, 设置优先级可能会获得更多的CPU时间片, 但是闲时优先级的高低几乎不会有任何作用。

3.2 线程优先级源码分析

  1. public final void setPriority(int newPriority) {
  2. ThreadGroup g;
  3. checkAccess();
  4. if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
  5. throw new IllegalArgumentException();
  6. }
  7. if((g = getThreadGroup()) != null) {
  8. if (newPriority > g.getMaxPriority()) {
  9. newPriority = g.getMaxPriority();
  10. }
  11. setPriority0(priority = newPriority);
  12. }
  13. }

通过上面源码的分析,我们可以看出,线程的优先级不能小于1也不能大于10,如果指定的线程优先级大于线程所在group的优先级, 那么指定的优先级将会失效, 取而代之的是group的最大优先级, 下面我们通过一个例子来证明一下:

  1. @Test
  2. public void TestSetPro() {
  3. ThreadGroup group = new ThreadGroup("test");
  4. group.setMaxPriority(7);
  5. Thread thread = new Thread(group, "test-group");
  6. thread.setPriority(10);
  7. System.out.println(thread.getPriority());
  8. }

上面的结果输出为7,而不是10,因为它超过了所在线程组的优先级别。

3.3 关于优先级的一些总结

一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了,那么线程默认的优先级是多少呢?
线程默认的优先级和它的父类保持一致, 一般情况下都是5, 因为main线程的优先级就是5,所以它派生出来的线程都是5

4.获取线程ID

  1. public long getId()

获取线程的唯一ID,线程的ID在整个JVM进程中都会是唯一的, 并且是从0开始逐次递增。如果你在main线程(main函数) 中创建了一个唯一的线程, 并且调用getId() 后发现其并不等于0, 也许你会纳闷, 不应该是从0开始的吗?之前已经说过了在一个JVM进程启动的时候, 实际上是开辟了很多个线程, 自增序列已经有了一定的消耗,因此我们自己创建的线程绝非第0号线程

5.获取当前线程

  1. public static Thread current Thread()

用于返回当前执行线程的引用, 这个方法虽然很简单,但是使用非常广泛,我们在后面的内容中会大量的使用该方法。

6.设置线程上下文类加载器

  1. public ClassLoader getContext ClassLoader()
  2. 获取线程上下文的类加载器, 简单来说就是这个线程是由哪个类加器加载的,如果是在没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器。
  3. public void setContext ClassLoader(ClassLoader cl)
  4. 设置该线程的类加载器, 这个方法可以打破JAVA类加载器的父委托机制, 有时候该方法也被称为JAVA类加载器的后门。

7.线程interrupt

  1. public void interrupt()
  2. public static boolean interrupted()
  3. public boolean is Interrupted()

7.1 interrupt

如下方法的调用会使得当前线程进入阻塞状态, 而调用当前线程的interrupt方法, 就可以打断阻塞。

  1. Objectwait方法。
  2. Objectwait(long) 方法。
  3. Objectwait(long int) 方法。
  4. Threadsleep(long) 方法。
  5. Threadsleep(long int) 方法。
  6. Threadjoin方法。
  7. Threadjoin(long) 方法。
  8. Threadjoin(long int) 方法。
  9. Interruptible Channelio操作。
  10. Selectorwakeup方法。
  11. 其他方法

上述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的interrupt方法, 则会打断这种阻塞, 因此这种方法有时会被称为可中断方法, 记住, 打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
一旦线程在阻塞的情况下被打断, 都会抛出一个称为InterruptedException的异常, 这个异常就像一个signal(信号) 一样通知当前线程被打断了, 下面我们来看一个例子:

  1. @Test
  2. public void TestThreadInterrupt() throws InterruptedException {
  3. Thread thread = new Thread(
  4. ()->{
  5. try {
  6. TimeUnit.MINUTES.sleep(1);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. );
  12. thread.start();
  13. TimeUnit.MILLISECONDS.sleep(2);
  14. thread.interrupt();
  15. }

上面的代码创建了一个线程,并且企图休眠1分钟的时长,不过很可惜,大约在2毫秒之后就被主线程调用interrupt方法打断, 程序的执行结果就是“Oh, iambe interrupted.”
interrupt这个方法到底做了什么样的事情呢?在一个线程内部存在着名为interrupt flag的标识, 如果一个线程被interrupt, 那么它的flag将被设置, 但是如果当前线程正在执行可中断方法被阻塞时, 调用interrupt方法将其中断, 反而会导致flag被清除, 关于这点我们在后面还会做详细的介绍。另外有一点需要注意的是,如果一个线程已经是死亡状态,那么尝试对其的interrupt会直接被忽略。

7.2 isInterrupt

isInterrupted是Thread的一个成员方法, 它主要判断当前线程是否被中断, 该方法仅仅是对interrupt标识的一个判断, 并不会影响标识发生任何改变, 这个与我们即将学习到的interrupted是存在差别的

7.3 interrupted

interrupted是一个静态方法, 虽然其也用于判断当前线程是否被中断, 但是它和成员方法isInterrupted还是有很大的区别的, 调用该方法会直接擦除掉线程的interrupt标识, 需要注意的是, 如果当前线程被打断了, 那么第一次调用interrupted方法会返回true, 并且立即擦除了interrupt标识; 第二次包括以后的调用永远都会返回false, 除非在此期间线程又一次地被打断,下面设计了一个简单的例子,来验证我们的说法:

7.4interrupt注意事项
打开Thread的源码, 不难发现, is Interrupted方法和interrupted方法都调用了同一个本地方法:

  1. private native boolean isInterrupted(boolean ClearInterrupted);

其中参数clearInterrupted主要用来控制是否擦除线程interrupt的标识。
isInterrupted方法的源码中该参数为false, 表示不想擦除:

  1. public boolean isInterrupted(){
  2. return isInterrupted(false);
  3. }

而interrupted静态方法中该参数则为true, 表示想要擦除:

  1. public static boolean interrupted() {
  2. return current Thread() .is Interrupted(true);
  3. }

image.png

8 线程join

  1. public final void join() throws InterruptedException
  2. public final synchronized void join(long millis int nanos) throws InterruptedException
  3. public final synchronized void join(long millis) throws InterruptedException

8.1线程join方法详解

join某个线程A, 会使当前线程B进入等待, 直到线程A结束生命周期, 或者到达给定的时间, 那么在此期间B线程是处于BLOCKED的, 而不是A线程, 下面就来通过一个简单的实例解释一下join方法的基本用法:
image.png
image.png

image.png

8.2 join方法结合实战

9 如何关闭一个线程

9.1 正常关闭

1.线程结束生命周期正常结束

线程运行结束,完成了自己的使命之后,就会正常退出,如果线程中的任务耗时比较短,或者时间可控,那么放任它正常结束就好了。

2.捕获中断信号关闭线程

我们通过new Thread的方式创建线程, 这种方式看似很简单, 其实它的派生成本是比较高的,因此在一个线程中往往会循环地执行某个任务,比如心跳检查,不断地接收网络消息报文等,系统决定退出的时候,可以借助中断线程的方式使其退出,示例代码如下:
image.png

上面的代码是通过检查线程interrupt的标识来决定是否退出的, 如果在线程中执行某个可中断方法,则可以通过捕获中断信号来决定是否退出。

image.png

3 使用volatile开关控制

由于线程的interrupt标识很有可能被擦除, 或者逻辑单元中不会调用任何可中断方法,所以使用volatile修饰的开关flag关闭线程也是一种常用的做法, 具体如下:

  1. static class MyTask extends Thread {
  2. private volatile boolean closed = false;
  3. @Override
  4. public void run() {
  5. System.out.println("I will start work");
  6. while( !closed && !isInterrupted()) {
  7. // 正在运行
  8. }
  9. System.out.println("I will be exiting");
  10. }
  11. public void close() {
  12. this.closed = true;
  13. this.interrupt();
  14. }
  15. }
  16. public static void main(String[] args) throws InterruptedException {
  17. MyTask t = new MyTask();
  18. t.start();
  19. TimeUnit.MINUTES.sleep(1);
  20. System.out.println("System will be shutdown");
  21. t.close();
  22. }

9.2 异常退出

在一个线程的执行单元中, 是不允许抛出checked异常的, 不论Thread中的run方法, 还是Runnable中的run方法, 如果线程在运行过程中需要捕获checked异常并且判断是否还有运行下去的必要, 那么此时可以将checked异常封装成unchecked异常(RuntimeException) 抛出进而结束线程的生命周期。

9.3 进程假死

相信很多程序员都会遇到进程假死的情况,所谓假死就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上它是没有死的,程序之所以出现这样的情况,绝大部分的原因就是某个线程阻塞了,或者线程出现了死锁的情况。

10本章总结

在本章中, 我们比较详细地学习了Thread的大多数API, 其中有获取线程信息的方法,如getId() 、getName() 、getPriority() 、current Thread() , 也有阻塞方法sleep() 、join() 方法等,并且结合若干个实战例子帮助大家更好地理解相关的API, Thread的API是掌握高并发编程的基础,因此非常有必要熟练掌握。