基本概念
Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联.
用上一篇中的例子来看,每个执行流,也需要有一个对象来描述,而Thread类的对象就是用来描述一个线程的执行流的,JVM也会将这些Thread对象组织起来,用于线程调度,线程管理.
Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
[了解]Thread(ThreadGroup group, Runnable target) | 线程可以用来分组管理(了解即可) |
注:一般自己创建的线程(没有命名),默认为Thread-0,1,2,3...
这个是不太好区分的,推荐利用上面命名的构造方法
Thread t1 = new Thread(); Thread t2 = new Thread(new MyRunnable()); Thread t3 = new Thread("这是我的名字"); Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否是后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
(1)ID是线程的唯一标识,不同线程是不会重复的(因为ID是jvm自动分配的身份标识,会保证身份的唯一性).
(2)名称是各种调试工具都要用到的重要属性
(3)状态表示线程当前所处的一个情况,之后我们将详细说明
(4)优先级高的线程理论上更容易被调度到(这个属性用的很少)
(5)isDaemon设置为true就是后台, daemon意为守护,可叫做守护线程(非常抽象),但更广泛的被叫为后台线程,与后台线程相对,还有前台线程.后台线程其运行不会阻止进程结束,前台线程其运行会阻止进程结束(jvm中内置的线程都是后台线程,不会阻止进程结束)关于后台线程需要记住一点:JVM会在一个进程的所有非后台线程结束以后,才会结束运行. 而我们代码创建的线程默认是前台线程,会阻止进程结束,只要前台线程没有执行完,进程就不会结束.即使main已经执行完毕.
(6)是否存活,最简单的理解,就是run方法是否运行结束了,高级的理解:就是表示内核中的线程(PCB)是否还存在
代码示例:
public class ThreadDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { for(int i = 0; i < 10; i++) { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + ": 我还活着"); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 我即将死去"); }, "一个不愿透漏姓名的神秘线程"); System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId()); System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName()); System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState()); System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority()); System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon()); System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive()); System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted()); thread.start(); while(thread.isAlive()) { System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState()); } } }
启动一个线程 - start()
之前我们已经看到了如何通过覆写run方法创建一个线程对象,但是线程对象被创建出来并不意味着线程就开始运行了.
start和run方法的区别.
1.覆写run方法是提供线程要做的事情的指令清单,调用run()实际上还是在main线程中,根本还是串行执行
2.使用start()start方法才是真正意义上在操作系统底层创建了一个新线程.
终止一个线程
认为是让线程run方法(入口方法)执行完毕
李四是X公司的一名员工,老板要求按步骤给某人转账.
按理来说他会正常按照行动指南上的步骤去进行工作,不完成是不会结束的.但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到了我们停止线程的方式了.
目前常见的有以下两种方式:
1.通过共享的标记来进行沟通
2.调用interrupt()方法来通知
示例1:使用自定义的变量作为标志位.
public class ThreadDemo1 { public static boolean isQuit = false; public static void main(String[] args) { Thread t = new Thread(() -> { while(!isQuit) { System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 啊!险些误了大事!"); }, "李四"); System.out.println(Thread.currentThread().getName() + ": 让李四开始转账."); t.start(); try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + ": 老板来电话了,得赶紧通知李四对方是个骗子!"); isQuit = true; } }
这个写法,不够优雅,Thread提供了更优雅的方法供我们选择.
示例2:使用Thread.interrupted()或者Thread.currentThread().isInterrupted()代替自定义标志位 .
Thread内部包含了一个boolean类型的变量作为线程是否被中断的标记.
方法 | 说明 |
public void interrupt() | 中断对象关联线程,如果线程正在阻塞, 则抛出InterruptedException异常并清除中断标志 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,并清除中断标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不会清除中断标志 |
使用thread对象的interrupted()方法通知线程结束
public class ThreadDemo3 { public static void main(String[] args) { Thread t1 = new Thread(() -> { while(!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); System.out.println(Thread.currentThread().getName() + ": 有内鬼,终止交易! "); //注意此处的break,后面要讲 break; } } System.out.println(Thread.currentThread().getName() + ": 啊!险些误了大事"); }, "李四"); t1.start(); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + ": 老板来电话了,得赶紧通知李四对方是个骗子"); t1.interrupt(); } }
Thread收到通知的方式有两种:
1. 如果线程因为调用wait/join/sleep等方法而阻塞挂起,则会以InterruptedException异常的形式通知,清除中断标志.
(在执行sleep的过程中,调用interrupt,大概率sleep休眠时间未到,被提前唤醒了.)
提前唤醒,会出现两件事:1.抛出InterruptedException异常;2.清除Thread对象的isInterrupted标志位(通过interrupt已经设为true了,但是sleep提前唤醒操作,又会将标志位设为false)
sleep清除标志位,是为了给程序员更多"可操作空间"
当出现InterruptedException时,要不要结束线程取决于catch中代码的写法,可以选择忽略这个异常,也可以跳出循环结束的线程
(1)加上break:让线程立即结束
(2)不加break:让线程不结束,继续执行
(3)写一些其它的代码再加break :让线程执行一些逻辑后结束
在实际开发中catch中的代码
(1)尝试自动恢复,就尽量自动恢复,比如出现了网络通信相关的异常,就在catch中尝试重连
(2)记录日志(将异常信息记录到文件中):有些问题不严重(不需要立即解决),先记下来,等程序猿有空再解决
(3)发出报警:比较严重了,将以短信电话等形式通知程序员,表明需要立即解决
(4)少数业务正常逻辑需要依赖catch
2.否则,只是一个内部的一个中断标志被设置,thread可以通过
Thread.currentThread().isInterrupted判定指定线程的中断标志被设置,不清除中断标志.这种方式通知收到更及时,即使线程正在sleep也可以马上收到.
等待一个线程 - join()
有时,我们需要等待一个线程完成它的工作之后,才能进行自己的下一步工作.而不是通过强行终止这个线程的方法(实际上强行终止一个线程也不科学).例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束.
举例:
public class TestDemo2 { public static void main(String[] args) throws InterruptedException { Runnable target = () -> { for(int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ": 我还在工作! "); Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(Thread.currentThread().getName() + ": 我结束了"); }; Thread t1 = new Thread(target, "李四"); Thread t2 = new Thread(target, "王五"); System.out.println("先让李四开始工作"); t1.start(); t1.join(); System.out.println("李四工作结束了,王五开始工作"); t2.start(); t2.join(); System.out.println("王五工作结束了"); } }
方法 | 说明 |
public void join() | 等待线程结束(就是死等,等不到不走) |
public void join(long millis) | 等待线程结束(最多等millis毫秒) |
public void join(long millis, int nanos) | 同理,可设置精度 |
获取当前线程的引用
利用下面的方法可以获得当前线程的实例,哪个线程调用就是哪个线程的实例.相信你们已经非常熟悉了.
public static Thread currentThread();
休眠当前线程
也是我们比较熟悉的方法,要记得:因为线程调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的.
方法 | 说明 |
public static void sleep(long millis)throws InterruptedException | 休眠当前线程millis毫秒 |
public static void sleep(long millis)throws InterruptedException | 更高精度 |