一、Thread 类
Thread 类我们可以理解为是 java 用于管理线程的一个类,里面封装了操作系统提供的线程管理这一方面的 API (Thread 是优化后的结果), Java 代码创建的每一个线程,可以理解为为 Thread 实例化的对象,Thread 对象用于描述线程的信息。
Java 标准库中 Thread 类可以视为是对操作系统对线程管理方面提供的 API 进行了进一步的抽象和封装.
API : Application Programing linerface
给你一个软件,你能对他干什么,基于它提供的这些功能,就可以写一些代码,然后封装在一起,方便别人使用。
编辑计算机通常只有一个CPU(多核心),单核心在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。
1.1 Thread 的常见构造方法
方法名 |
解释: |
Thread() |
创建线程对象 |
Thread( Runnable target ) |
使用 Runnable对象创建线程对象 |
Thread( String name ) |
创建线程对象,并为其命名 (方便辨认) |
Thread(Runnable target, String name) |
使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target) |
线程可以被用来分组管理,分好的组即为线程组。 |
二、 Thread 的常见属性
属性 |
获取方法 |
ID |
getId() |
名称 |
getName() |
状态 |
getState() |
优先级 |
getPriority() |
是否后台线程 |
isDaemon() |
是否存活 |
isAlive() |
是否被中断 |
isInterrupted() |
返回对当前正在执行的线程对象的引用 |
Thread.currentThread |
ID :每个线程创建的是时候都会有一个唯一的 id,不同线程不会重复
名称:给每个线程起一个别名,例如:语言通话进程,文字交流进程,动态分享进程,调试进程的时候方便辨别。
2.1 启动一个线程
java 代码创建线程的方法主要有三种
自定义类继承 Thread 类 ,重写父类 run 方法
自定义类实现Runnable 接口,重写 run 方法
lambda 表达式,不依托类,直接指向 run 方法重写
采用 lambda表达式创建线程对象
publicstaticvoidmain(String[] args){ Thread t =newThread(() -> { //采用 lambda表达式创建线程对象 System.out.println("线程 t 启动"); }); t.start();// 启动线程,从启动开始就有两个线程流参与 CPU 的调度执行 System.out.println("主线程 main"); }
我们创建了一个线程对象,然后重写父类的 run 方法,这并不意味着线程就可以运行了。
线程的 run 方法, 我们可以理解为主线程的 main() 方法, 我们刚开始接触编程的时候,应该都听过程序是从 main () 函数开始执行的,此时 run 方法,就可以认为是 另一个线程的 main() 方法,多线程并发执行。run 方法中就是我们程序的另一个执行流。
例如: qq 在打视频电话的时候,同时也可以接收qq 消息,视频电话是一个线程,聊天窗口也是个线程,并发执行,互不干扰,此时我们把 qq 聊天窗口当作是 main() 方法,qq 视频通话当作是 t 线程的 run() 方法, 两个线程同属于 qq 这个进程, 当我们 启动qq 时,(一个进程中必须包含一个线程,线程是系统调度的基本单位)默认启动的是主线程执行main()方法 , qq 一打开,聊天消息响不停,没有问题,我们不启动qq 视频电话或者是没有好友打来电话,t 线程不会被启动,这并不意味着 qq视频通话的功能不存在,需要的是手动或者是被动的启动, 那么 t 线程启动的方式就是调用我们的 t.start() 方法 ,此时 t 线程才是真正的独立的执行了,我们点击视频通话的操作就可以想象为调用了线程的start 方法启动。
调用了 start() 方法,此时真正的在操作系统的底层创建出一个可以被调度执行的线程。
2.2 获取当前线程的引用 currentThread()
publicstatic Thread.currentThread(); //返回当前线程对象的引用
2.3 休眠当前线程 sleep()
那个线程调用该方法就会进入休眠状态(阻塞),该进程暂时不参加系统的调度。该方法会抛出InterruptedException 异常
public static void sleep(long millis) throws InterruptedException |
休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throwsInterruptedException |
可以更高精度的休眠 |
2.3 终止一个线程
一个线程启动之后,进入工作状态,就会按照我们设计的程序功能去执行,没有执行完毕也不会无缘无故的结束掉线程,以上述qq 视频通话为例, 打qq 视频通话,需要我们手动启动qq 视频通话,或者好友打来视频电话,这涉及到线程的启动(调用 start () 方法)。
如果我们想要结束掉视频通话,一般情况下有两种方法
就需要手动点击挂断,通话双方都可,意思就是线程可以主动的结束(我挂断),也可以被动的结束(好友挂断),无论是是挂断电话,线程都会结束。
无可抗因素,例如,手机断网,当我们手机没有网络支持,通话自然而然结束,或者是网络特别卡顿,也有可能结束掉线程,再或者是手机内存不够,系统无法继续为qq 提供内存资源使其运行,会强制中止 qq 的进程,一般是直接干掉进程(进程是操作系统分配资源的基本单位,进程包含线程,多线程共享进程资源)。
那我们java 代码在没有 (bug)的情况下如何终止进程,常见的有两种处理方法:
1. 共享标记来进行沟通
2. 调用 interrupt() 方法来通知线程该结束了
2.2.1 共享标记
publicclassDemo1 { privatestaticboolean flag = true; //共享标记 publicstaticvoidmain(String[] args) throws InterruptedException { Thread t =newThread(() -> { //采用 lambda表达式创建线程对象 while(flag) { // 使用共享标记可以由其他线程中止本线程 try{ Thread.sleep(200); }catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程 t 执行"); } }); t.start();// 启动线程,从启动开始就有两个线程流参与 CPU 的调度执行 System.out.println("主线程 main 一秒后中止 t 线程"); Thread.sleep(1000); //线程休眠一秒后再被CPU调度执行 flag =false; System.out.println("t线程已被中止"); } }
我们定义一个boolean类型 的成员变量 flag 用于条件判断终止线程,条件判断然后 return 、抛异常都可以。
运行结果:
运行结果有些意外,为什么当 main 线程中打印了 t 线程已被中止, 最后线程 t还打印了一个 线程 t 执行呢? 这个问题与系统的调度有一定的关系,两个线程并发执行,可能是由一个 cpu核心处理,那么这两个线程就在 cpu 上切换执行,完全有可能,t线程已经完成了第五次的条件判断,还没来的及打印, cpu 就执行main 线程 修改flag = false,然后打印,此时再切换为 t 线程 ,继续打印,再从 flag 读取 boolean 值进行条件判断就不符合条件了,当然 系统怎么调度的的线程,执行顺序是无序的,会有多种可能,而且 线程可以迸发执行,共享一个控制台的话,打印是必须得分个先后得,所以这种情况是正常得。
关于将 flag 设置为成员变量的问题:
那能不能将 flag 定义在 main() 方法内部, 线程 t 能不能访问到 flag 呢, 答案可以的。
但是这里报错了,原因是:lambda表达式中使用的变量应该是final或有效final,意思就是只能使用不发生改变的属性,如果我们 main 中只定义 flag = true; 并不对 flag 值进行修改,那么 lambda表达式中就可以访问到 flag 。
以上代码大致可以理解一下Java 代码线程终止线程的一种方式,写的有些简陋可能会涉及到线程安全的问题。
2.2.2 调用 interrupt() 方法
刚刚采用的 共享标记的方法终止线程,共享标记需要我们手动的创建, interrupt() 方法是 Thread 类内置的一个方法,方法内部提供一个标志位,所以我们只需要调用该方法就可以实现终止线程的效果。
interrupt() 方法对调用线程设置标志位
使用 Thread.interrupted() 或者Thread.currentThread().isInterrupted() 判断线程是否创建了标志位
Thread.currentThread() : 返回对当前正在执行的线程对象的引用,针对于该线程判断是否创建了标志位
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
方法 |
说明 |
public void interrupt() |
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() |
判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() |
判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
我们使用 isInterrupted() 方法来判断是否设置了标志位(没有是 false),用interrupt() 方法进行标志位设置 true。
publicclassDemo2 { publicstaticvoidmain(String[] args) throws InterruptedException { Thread t =newThread(() -> { //采用 lambda表达式创建线程对象 while(!Thread.currentThread().isInterrupted()){ //isInterrupted() 判断是否设置了标志位 //!Thread.interrupted(); try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程 t 执行"); } }); t.start();// 启动线程,从启动开始就有两个线程流参与 CPU 的调度执行 System.out.println("主线程 main 3秒后中止 t 线程"); Thread.sleep(3000); //线程休眠一秒后再被CPU调度执行 t.interrupt(); // 设置把标志位,并将标志设置为true; } }
Thread 接收到的是否有标志位通知有两种情况:
如果线程是因为 调用了sleep、join 等方法的原因阻塞(只要是阻塞,阻塞可以看作线程暂时不参加 CPU 的调度了),然后 t. interrupt() 方法设置标志,那么会通过抛出 InterruptedException 异常的形式通知,将线程唤醒,此处博主使用sleep() 使得 t 线程休眠(阻塞一秒),主线程(main)调用方法设置标志后线程被强制唤醒,sleep() 会将标志位置为空 (标志位置为 false),意思就是相当于没设置标志,isInterrupted() 方法检查没有标志位,返回 false, 然后方法前面!(非),结果返回 true, 线程不会终止。
当抛出 InterruptedException 异常的时候,要不要结束线程取决于 catch 中代码的写法.,可以选择忽略这个异常(博主这里直接将异常抛出,所以标志位就被清空了), 也可跳出循环结束线程(break)。
没有阻塞等特殊情况,有两种方式判断是否存在标志位:
1)Thread.interrupted()判断当前线程的中断标志被设置,清除中断标志 (清除后返回false)* 2)Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
那么博主这段代码明显是第一种方式,设置一个标志位也是一波三折,看看运行结果:
抛出异常后 t 线程继续执行,说明抛出异常后,标志位被清除了,t 线程是循环一次sleep 阻塞一秒嘛,所以打印了两个结果,第三秒,主线程给 t 线程设置标志位(本意结束 t 线程),结果直接唤醒 阻塞中的 t 线程,从sleep状态被唤醒后,sleep 将 isInterrupted() 的标志位清空,导致循环无法结束。
阻塞状态被设置标志位唤醒,将标志位清空的目的也是为了线程对何时结束有一个灵活的掌控, 如果上述状态我们对sleep 进行异常处理 carch 里面添加break;或者是 return; 都是可以的比较灵活。
调用 interrupt() 方法终止线程本质上还是设置标志位,不是说直接将线程干掉,而是程序通过标志位的信息可以进行终止线程的操作。
两个人打qq 视频电话,你不挂断,我也不挂断,那就一直死循环,挂断就相当于对这个循环按break; 循环结束,如果该线程没有什么其他程序要执行的话启动周期也结束了。
2.3 join() 等待一个线程
等待一个线程,预设一个场景:
在只有一个厕所的情况下,李四只有等待张三(线程)执行完毕后才能上厕所(被调度),期间李四只能是耐心等待,也不能干其他的事。
同一进程下,多线程的调度是并发执行(CPU 来回切换执行线程),操作系统对线程的调度是无序的,每个线程被执行的时间也是未知的,无法判断线程之间的执行的先后顺序,那么使用 join 方法就可以指定那个线程等待那个线程执行完毕。
publicstaticvoidmain(String[] args) throwsInterruptedException { Thread t =newThread( () -> { for(int i = 0; i < 5; i++) { try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程 t 执行"); } }); t.start();//启动线程 t for (int i= 0; i < 5; i++) { Thread.sleep(1000); System.out.println("主线程 main 执行"); } }
运行打印结果有时候 main 线程前,有时候 t 线程在前,这也说体现了线程的调度是无序的这个概念,添加 sleep函数的目的就是使得线程休眠一秒再执行,因为CPU 的执行速度很快,数据量小无法观察到这种并发调度的状态。
假设现在我们想让 main 主线程等待 t 线程执行完毕后再执行 自身的操作,就可以使用 join 方法。
很明显,使用 join 方法后线程的调度是有序的,但是此时 两个线程不再是并发执行,而是串行执行了,在main 线程中 调用了 t. join 方法,意思就是 让main 线程等待 线程 t 执行完毕后,再往下执行, 谁调用,谁等待,等待的一方,直接进入 Blocked 阻塞状态,阻塞可以看作线程暂时不参加 CPU 的调度执行。
main 线程调用 t.join 的时候,如果 t 正在运行,此时 main 线程直接进入阻塞态,直到 t 线程执行完毕(run 方法执行完了),main 线程才会解除阻塞,继续参与系统调度,CPU 执行。
如果 t 线程一直在执行,那么 main 线程就会一直处于阻塞状态,这谁能忍,所以 join 还有另一个重载后的方法,可以在调用 join 时提供一个最大等待时间的参数,超出等待时间 main 线程也可以解除阻塞态。
方法 |
说明 |
public void join() |
等待线程结束 |
public void join(long millis) |
等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) |
同理,更高精度 |