Thread类
目录
创建线程
一、继承Thread类
二、实现Runnable接口
三、匿名内部类
(1)Thread匿名内部类
(2)Runnable匿名内部类
四、lambda表达式
Thread类的方法
给线程命名
IsAlive()方法
setDaemon()方法
currentThread()方法
终止线程
(1)使用全局变量(成员变量)
(2)Thread提供interrupt()方法
线程休眠
线程等待
(1)无参join
(2)带参join
线程是操作系统提供的概念,操作系统提供了一些api供程序员使用,不过这些原生线程api是用c语言编写的,而且不同操作系统的线程api也不一样,java对操作系统中的api进行了统一的封装,于是出现了Thread类。
创建线程
一、继承Thread类
class Mythread extends Thread {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Thread t = new Mythread();//真正在系统中创建一个线程t.start();while (true) {System.out.println("hello main");}}
}
上述代码创建了一个类Mythread继承Thread,并且重写了Thread类的run方法
在main方法中实例化Mythread类并向上转型,用对象引用t调用Thread类中的start()方法。
t.start()这句代码执行后真正的在系统中创建了一个线程(JVM调用操作系统的api完成线程的创建)
每个Thread对象都只能start一次,每次想创建一个新的线程,都得创建一个新的Thread对象(不能重复利用)
run方法不需要我们去调用,例如:t.run,这样虽然能打印出内容,但系统中并没有创建出这个线程。当我们使用t.strat()时,会自动的调用run方法,并且还会创建出线程。
二、实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {Runnable r = new MyRunnable();Thread t = new Thread(r);t.start();System.out.println("hello main");}
}
在MyRunnable类中也要重写接口Runnable的run方法
最终还是要通过Thread类去创建线程,但是通过Runnable接口这样写的好处是:
代码具有低耦合的特点,让要执行的任务本身和线程这个概念能够解耦合,后续如果要变更代码(比如不通过线程执行这个任务,通过其它方式...)采用Runnable这样的方案,代码的修改就会简单很多。
三、匿名内部类
(1)Thread匿名内部类
public static void main(String[] args) {//匿名内部类Thread t = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello Thread");}}};t.start();while (true) {System.out.println("hello Main");}}
}
这种方法是不再创建一个新的类去继承Thread类,而是在实例化的时候在后面写一个大括号,在大括号里面重写run方法。
这相当于是创建了Thread类的子类,但是这个子类的没有名字,称之为匿名内部类。
(2)Runnable匿名内部类
public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}};Thread t = new Thread(runnable);t.start();while (true) {System.out.println("hello main");}}
与Thread内部类一样,除了多了一句Thread t = new Thread(runnable);代码
四、lambda表达式
这个方法是笔者认为最舒服,最方便的写法
它用到了一个函数式接口:() -> {}
Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");}});
这个函数式接口要放在Thread的括号种,并且需要注意的是lambda表达式中的定义是在new Thread之前的,也就是在Thread t声明之前的。
上面介绍了线程创建的三种方法,我们需要知道main也是一个线程,可以称之为主线程,我们按照以前的认知,可能会觉得main线程结束了,程序就结束了(只针对单线程程序),在多线程中其实main线程结束和其它线程没有什么关系,等到所有的线程都运行结束了,这个程序才结束。
一个程序可以创建多个线程,主线程是这个程序最先开始运行的(因为要在主线程中去启动其它线程),但不代表主线程运行完其它线程才开始,各个线程之间都是并发执行的方式(串行执行和并行执行混合使用)。
Thread类的方法
给线程命名
Thread t1 = new Thread(() -> {while (true) {System.out.println("hello a");}}, "a");Thread t2 = new Thread(() -> {while (true) {System.out.println("hello b");}}, "b");
首先我们要明白,Thread类后面的t1不是线程的名字,这只是线程声明的时候创建的一个变量,用来存储线程的引用。
在Thread()的括号内,第二个参数用来设置这个线程的名字,此时第一个线程名字是a,第二个线程的名字是b。
我们可以通过javajdk自带的一个软件去查看系统中正在执行的线程,但这个只能去查看java代码编写的线程:
我们此时编写以下两个线程:
上述代码中加入了sleep操作,是为了每隔1秒打印一次,否则打印太快,占用cpu资源会很大
首先在你的电脑上找到你jdk安装的位置:默认是C:\Program Files\Java
然后选中你这个idea正在使用的jdk版本,笔者使用的是jdk-17
点开jdk-17后选中bin目录:
然后找到jconsole.exe这个程序:
点开之钱我们要确保我们的线程一直在运行,就是让上述代码一直运行
点开后选中这个代码所在的类:
点击连接后再选择不安全的连接:
点击线程:
可以看到我们创建的三个线程:
它们的名字分别是a、b、c
IsAlive()方法
可以判断系统中的线程是否存活
java代码中创建的Thread对象和系统中的线程是一一对应的,但Thread对象的生命周期和系统中的线程生命周期是不同的,可能存在系统中的线程已经销毁,但Thread类的对象还存在。
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {//三秒后线程销毁for (int i = 0; i < 3; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程结束");});//启动线程t.start();//等待t线程运行完毕主线程才开始运行t.join();System.out.println(t.isAlive());}
上述代码就表明了虽然t线程在系统中被销毁了,但是这个Thread类的对象t还可以使用,可以通过isAlive去判断系统中的这个线程是否存在。
setDaemon()方法
这个方法可以将前台线程改为后台线程
首先我们要先知道什么是前台线程,后台线程。
前台线程就是一个进程结束不结束的关键,假设在一个进程中有t1、t2、t3这三个线程,当这个三个线程都结束了,这个进程才会结束。
什么是后台线程呢?后台线程可以称之为守护线程,顾名思义,它要等到前台线程都结束了,它才会结束,而且它的结束也不会影响到这一整个进程的结束与否,后台线程起到辅助这个进程的作用。
能影响到进程的结束就是前台进程,当前台进程都结束了,后台进程才会结束
上面的图是线程监视控制台,其中a、b、c是我们创建的线程,称之为前台线程。除了它们其它的线程都是后台线程,这些是JVM提供的线程,这些线程具有特殊功能比如垃圾回收线程等,它们的存在不影响进程结束,它们会随着进程的结束而结束。
我们可以通过t.setDaemon(true)这行代码,将t线程从前台线程转为后台线程。
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//把t设为后台线程t.setDaemon(true);t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");Thread.sleep(1000);}System.out.println("main结束");}
currentThread()方法
这个方法是一个静态方法,通过Thread类调用后返回当前这个线程的实例,在哪个线程中调用就返回哪个线程的实例。
Thread.currentThread();
终止线程
终止线程表示让这个线程直接停止,不会恢复。
(1)使用全局变量(成员变量)
定义一个全局变量(成员变量):
private static boolean isFinished = false;
初始值设置为false,需要停止某一个线程时,将这个全局变量的值改为true
完整代码:
public class Demo7 {private static boolean isFinished = false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!isFinished) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread进程结束");});t.start();Thread.sleep(3000);isFinished = true;}
}
这个代码表示在t线程中每隔1秒打印一次“hello thread”,在主线程中3秒后让t线程结束。
为什么一定要定义一个全局变量,而不能将isFinished变量放入main方法中当作局部变量呢?
这里涉及到一些很细的知识点:
“变量捕获”
在上述代码中,我们用lambda表达式创建的线程,在lambda表达式中,如果希望使用到表达式外面的变量,会触发“变量捕获”这样的语法,原因是lambda是回调函数,执行时机是很久之后,操作系统真正创建出线程后才会执行,很有可能main线程都执行完了,main方法中的变量都销毁了,此时这个变量isFinished就没了。所以为了解决这个问题,Java的做法是,将被捕获的变量复制一份到lambda中,后续lambda外面的变量是否销毁就无所谓了。
但在lambda表达式中对这个局部变量进行修改时会触发以下问题:
这是由于Java通过变量捕获,将这个局部变量复制了一份到lambda表达式中,当在lambda中修改这个变量时,会出现同一个变量有两份,值分别不同的问题。所以java不允许在lambda中修改捕获后的变量。
拷贝意味着这样的变量就不适合进行修改,修改一方,另一方不会随之变化,这种一边变,一边不变,非常的奇怪,所以Java这里就压根不允许你进行修改,不让修改我们就无法让线程进行终止,所以局部变量这个方法是不行的,必须去使用全局变量。
把局部变量改成成员变量后,不再是“变量捕获”语法,此时切换成了“内部类访问外部类的成员”语法。
lambda本质上是函数式接口,相当于一个内部类,isFinished变量本身就是外部类Demo7的成员
内部类本来就能访问外部类的成员,而且成员变量的生命周期是让GC(垃圾回收)来管理的,
所以在lambda里不用担心变量声明周期失效的问题,也就不必拷贝,不必限制final之类的。
(2)Thread提供interrupt()方法
Java中提供了现成的变量,直接进行判定即可。
需要以下两个方法配合使用:
interrupt() //终止线程(设置标志位的值(boolean类型))
isInterrupted() //判断线程是否被终止
这两个方法都需要线程的实例去调用
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e);break;}}System.out.println("终结成功");});t.start();Thread.sleep(3000);System.out.println("尝试终结t线程");t.interrupt();}
上述代码中,在lambda表达式中由于lambda表达式中的定义是在new Thread之前的,也就是在Thread t声明之前的,所以无法直接使用t去调用,但是可以通过:Thread.currentThread()
但是当我们去使用interrupt方法去主动进行终止时,代码会报出以下错误:
这是由于interrupt除了设置boolean变量(标志位),还能唤醒sleep这样的阻塞方法
sleep的时间还没结束,但被提前唤醒
当在t线程中正在sleep时,interrupted可以提前唤醒sleep,此时sleep会抛出异常
在我们的上述代码中,如果线程不结束我们每隔1秒打印一次输出到控制台,main线程中过了3秒会主动去终止t线程,此时的t线程可能刚好正在休眠,但被提前唤醒,由于try / catch原因报出sleep interrupted的错误,但我们不希望报出错误,因为终止线程我们不需要去注意这个线程是否在休眠,我们希望该终止时立刻终止,所以只需将catch的代码块中将 throw 一个异常改为break即可。
如果没有break,这个线程就不会结束,就会一直运行下去。
所以Java中的线程终止,不是一个强制性的措施,不是main让t终止,t就一定终止,选择权在t自己手上(catch中的代码)。
catch中可以有以下做法:
1.加上break(立即终止)
2.啥都不写(不终止)
3.catch中先执行一些其它的逻辑再break(稍后终止)
线程休眠
Thread.sleep()
用Thread类调用sleep方法,括号内的参数是时间,单位是ms
调用此方法并传入时间,此时这个线程会休眠相应的时间,休眠意味着这个线程被操作系统“挂起”,此时这个线程不会占用CPU资源,操作系统可以将CPU资源分配给其他线程。
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
上述代码中t线程每隔1000ms也就是1秒打印一次,主线程是每隔3秒打印一次
调用sleep方法需要处理异常,但是在lambda表达式中只能用try/catch进行处理,因为在lambda表达式中重写了run方法,重写的性质是只能改变方法内容,不能改变方法的外观(不能在重写的run方法后throw一个异常),在main方法中可以直接throw一个异常。
其实在sleep的参数中传入1000ms,线程实际的休眠时间可能要比1000ms多一点,代码调用sleep相当于是让当前线程让出cpu资源,后续时间到了,操作系统重新把这个线程调到cpu上,才能继续执行。但是时间到,意味着这个线程允许被操作系统调度了,而不是立即执行了。
线程等待
需要用到join方法,它是Thread类的一个实例方法
t.join();
(1)无参join
join方法能够实现多个线程之间结束的先后顺序
比如在主线程中调用t.join()方法就是让主线程等待t线程结束后,主线程再开始运行
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();t.join();System.out.println("main线程结束");}
上述代码表示,等到5秒后t线程结束,main线程才能开始执行
(2)带参join
join的括号内可以传入参数,比如在main方法中调用t.join(3000),表示main线程最多等待t线程3秒,3秒过后main线程也开始执行。main线程不管t线程有没有结束,最多等待它3秒。
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();t.join(2000);System.out.println("main线程结束");}
上述代码表示main线程在等待t线程2秒后,开始运行,过了两秒后main线程结束,打印到控制台如下:
结束~~~