多线程——Thread 类的基本用法
线程Tread类介绍:
我们知道,线程是操作系统提供的概念,操作系统一般会提供一些api(Application Programming Interface)来给程序员进行有关线程的操作,每个操作系统提供的线程api都不一样。为了解决统一性的问题,java中就独立封装一个Thread类,以便与程序员可以直接使用这个类来操作线程。
Thread类是Java多线程编程的核心类,位于java.lang包中。 Thread类与Runnable接口一起,为Java程序提供了创建和管理线程的能力。线程是程序中独立执行的路径,允许程序同时执行多个任务,从而提高程序性能。
线程从创建到消亡会经历不同的状态,包括创建、就绪、运行、阻塞、主动睡眠、等待唤醒和消亡。线程的上下文切换在多核处理器系统中提高了程序的并行执行效率。
由图可知,Thread类是实现Runnable接口的,而Runnable接口里有一个抽象方法run。可以理解为Runnable是一个任务,Thread类的构造方法中也可以直接传一个Runnable类型的参数进去实现线程的run方法,启动线程是直接执行run方法。
以下是Thread类的构造方法和run方法的介绍:
以下是Thread类的几个常见属性
1.ID是线程的唯一标识,不同线程不会重复
2.名称是其他的调试工具用到
3.状态表示当前线程处在什么样的情况如:就绪、运行、阻塞、主动睡眠、等待唤醒和消亡等情况
4.优先级高的理论上容易先被调度到
5.JVM会在一个进程的所有非后台线程结束之后,才会结束运行,后台线程的提前结束不影响。
6.我们自行创建的线程一般都是前台线程,我们可以自行修改为后台线程,调用setDaemon()方法,参数为true,则设置为后台线程
7.是否存活简单说明就是run方法是否执行结束了
线程创建
在线程main中,创建的线程是和main是并发执行的,由于调度问题执行顺序也会有所差异。
我们通过线程调用start方法来启动线程:
线程名.start();
(这才是真正的创建线程,JVM会调用操作系统的API来完成线程的创建,同时会执行线程的入口方法run,线程创建好后自动去调用)
创建线程有许多种方法,以下我们就介绍五种方法:
1.继承 Thread, 重写 run
创建MyTread类的实例
调用start方法启动线程
2.实现 Runnable, 重写 run
传参一个Runnable类
3.继承 Thread, 重写 run, 使用匿名内部类
4.实现 Runnable, 重写 run, 使用匿名内部类
5.使用 lambda 表达式
一起启动上述代码的五个线程,包括main线程,可以看到下图,都是并发执行:
线程中断
我们前面已经介绍了启动线程,现在讲解一下线程的终断:
所谓线程的中断,并不是单纯的中途暂停,而是彻底终止。
我们先来看个俩个例子:
1.将使用改变变量(标志位)的方式结束线程:
定义俩个线程:
先在外层定义一个全局变量(标志位),然后在t2线程通过修改标志位的方式终止t1线程。
可以看到,我们成功终止掉了t1线程,但是这里有个问题:
能否使用局部变量呢?
如图所示,我们要是将标志位改成局部变量的话,这里会有个报错信息。
我们要先了解到,lambda表达式可以当做一个回调函数,一般的执行时机是在很久之后(当操作系统真正创建线程之后,才会执行),可能线程创建完,main线程就已经结束了,对应的局部变量就已经销毁了。
为了解决这样的问题,所以java中涉及到一个“变量捕获”的操作,就是把被捕获的变量拷贝一份给lambda表达式中,无论外面的变量是否销毁,lambda都能正常使用。
但是有个前提,被捕获的变量,这个变量要不然就是被final所修饰,要不然就是在使用前确保不被修改,若是像如图的t2线程里一样修改了,会导致拷贝前和拷贝后的变量数值不一致,从而出现问题,若是像t1一样没有修改,虽然说成员变量已经销毁,但是只要lambda表达式没有被执行,或者没有被GC垃圾回收,这些变量会继续存在
这是使用局部变量所需要注意的问题,我们可以使用volatile这个关键字修饰局部变量,使这个共享变量具有可见性,保证修改的值会立刻更新到主内存中(这个后续在解决线程安全问题会提到)。
我们之前使用的成员变量作为标志位的方法,这个就不会涉及到“变量捕获”,而是内部类访问外部类的成员,lambda本质上是一个函数式接口,可以当成内部类,内部类可以直接访问外部类的成员变量,不用局限于final。
2.使用Thread类自带的功能代替标志位
我们先来看看常见的几种中断线程的方法
interrupted是静态方法,由类名直接调用即可,而isInterrupted是类方法,需要获取当前的对象再进行调用(获取当前对象方法:Thread.currentThread())
但是当我们运行起来又会发现一个问题:
在这里我们发现sleep抛出了一个异常,表示sleep被异常唤醒了。
原因是因为在t1循环中大部分时间都在sleep休眠状态,此时我们突然调用了interrupt方法终止了t1线程,t1线程被异常唤醒终止导致了sleep抛出了异常。
在catch中我们做的只是向更高一级抛出异常,最终会使JVM发现异常从而报错。
如果我们在catch这里什么都不做呢?
可以看到当我们终止了线程之后t2线程结束,但是t1线程好像不听使唤还在继续运行。
这是因为调用interrupt后标志位被设置成了true,但是会导致sleep提前唤醒,sleep提前唤醒之后导致线程继续执行就把标志位设回了false,下次判断就依旧继续执行。
要想解决上述问题,我们可以处理一下sleep的异常,就是跳出循环:
可以看到,sleep的异常问题就算是解决了。
线程等待
在多线程中,我们可能需要等待另外一个线程完成工作以后才继续执行当前线程,因此我们需要一个方法来完成这样的工作。
t.join();(在当前线程等待一个t线程)
示例:让main线程等待t1线程
可以看到,待t1线程执行完才结束main线程,其他带参数版本也是如此,若是怕等待时间过长可以设计最大等待时间。
线程休眠
获取线程引用
public staticThread currentThread();返回当前线程对象引用
例:
这里的Thread可以看做和this一样的用法,代表当前的引用对象