💐个人主页:初晴~
上篇文章我们简单介绍了什么是进程与线程,以及他们之间的区别与联系,实际应用中还是以多线程编程为主的,所以这篇文章就让我们更加深入地去剖析多线程编程的具体应用吧
目录
[二、 Thread 类及常⻅⽅法](#二、 Thread 类及常⻅⽅法)
[1、 Thread 的常⻅构造⽅法](#1、 Thread 的常⻅构造⽅法)
[2、 Thread 的⼏个常⻅属性](#2、 Thread 的⼏个常⻅属性)
一、初识Thread类
⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.
Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤(例如 Linux 的 pthread 库).
1、创建线程
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装
接下来我们就来看一下创建线程的几种写法吧:
(1)继承Thread类
编写的MyThread需要继承Thread类,不需要导包,因为Thread类是java.lang中内置的类。
继承不是主要目的,主要是为了重写Thread类中的run 方法,在其中写入所创建线程需要执行的逻辑语句。
若要让线程运行,需先实例化 编写的MyThread类,接着调用start方法就会在进程内部创建一个新的线程,新的线程就会执行刚才run里的代码。
具体代码如下:
java
class MyThread extends Thread{
//重写run方法
@Override
public void run(){
//线程执行的逻辑
System.out.println("Hello World!");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread=new MyThread();
//创建线程
myThread.start();
}
}
这个代码,运行起来是一个进程,但这个进程包含了两个线程。
1、调用main方法的线程被称为"主线程",之前提过一个进程中至少有一个线程,这个线程就是主线程
2、调用myThread.start()方法时会手动创建一个新的线程
主线程和新线程会并发/并行地在CPU上运行
不过,上述代码还不能很好地体现多线程编程的并发性与随机性,接下来用一个更加形象的代码表示一下:
java
class MyThread extends Thread{
//重写run方法
@Override
public void run(){
while (true){
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t=new MyThread();
//创建线程
t.start();
while (true){
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
可以发现,多个线程之间,谁先去CPU上调度执行,这个过程是 "不确定的" ,这个调度顺序取决于操作系统内核里的 "调度器" ,调度器里有一套规则,但是对于应用程序开发,无法进行干预,也无法察觉,因此把这个过程近似于 "随机" ,多线程的运行调度也被称之为 "抢占式执行"
注意:
上述代码中,并没有直接手动调用run方法,但是也被执行了。像run这种,用户手动定义 了,但是没有手动调用 ,最终被系统/库/框架调用执行了的方法,被称为**"回调函数(call back)"**。
(2)实现Runnable接口
Runnable就是用来描述"要执行的任务"是什么
java
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello Runnable!");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable=new MyRunnable();
Thread myThread=new Thread(runnable);
//创建线程
myThread.start();
}
}
通过Thread创建线程,而线程要执行的任务是通过Runnable来描述的,而不是通过Tread自己来描述,这样能起到一定的"解耦合"的作用,便于代码后期维护。
Runnable只是描述了一个任务,并不与"线程"强相关,后续执行这个任务的载体可以是线程,也可以是其他东西,比如线程池、虚拟线程(协程)等,一定程度上提高了代码的复用率。
对⽐上⾯两种⽅法:
• 继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
• 实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤Thread.currentThread()
(3)匿名内部类
匿名指没有类名,内部类指定义在其它类内部的类,匿名内部类一般就是"一次性"使用的类,用完就丢掉,相对来说内聚性会更好一些
匿名内部类创建 Thread ⼦类对象 :
java
// 使⽤匿名类创建 Thread ⼦类对象
Thread t=new Thread(){
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
这个方法本质上和(1)是一致的,具体原理如下:
1、定义匿名内部类,这个类是Thread的子类
2、类的内部,重写了父类的run方法
3、创建了一个子类的实例,并把实例的引用赋值给了t
匿名内部类创建 Runnable ⼦类对象:
java
//使⽤匿名类创建 Runnable ⼦类对象
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
lambda表达式创建 Runnable 子类对象:
java
Thread t3=new Thread(()-> System.out.println("使⽤lambda表达式创建 Runnable ⼦类对象"));
Thread t4=new Thread(()->{
System.out.println("使用lambda表达式创建 Runnable 子类对象");
});
lambda表达式本质上就是匿名内部类的更简化 的写法,很多时候,写匿名内部类都不是为了写"类",而是写类中的**"方法"** ,而lambda就可以避开类而直接描述其中的run方法
2、多线程的优势
可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
• 使⽤ System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
• serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算
java
public class ThreadAdvantage {
// 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使⽤并发⽅式
concurrency();
// 使⽤串⾏⽅式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利⽤⼀个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运⾏结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串⾏: %f 毫秒%n", ms);
}
}
二、 Thread 类及常⻅⽅法
Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。
1、 Thread 的常⻅构造⽅法
代码事例:
java
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
如果不给线程起名字,那么默认就会是叫做Thread-0,Thread-1......
给线程起名字,并不会影响线程的执行效果,不过起一个合适的名字,更有利于调试程序
ThreadGroup线程组是把多个线程放在一组里,方便统一地设置线程的一些属性。不过现在很少会使用线程组了,线程相关属性用到也不多,更多的是会使用"线程池"
2、 Thread 的⼏个常⻅属性
• ID 是JVM自动分配的,是线程的唯⼀标识,不同线程不会重复
• 名称是各种调试⼯具⽤的,Thread对象的身份标识
• 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
• 优先级⾼的线程理论上来说更容易被调度到
• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
• 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
• 线程的中断问题,下⾯我们进⼀步说明
关于前台线程与后台线程:
后台线程:如果这个线程执行过程中,不能 阻止进程的结束,(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就被称为"后台线程",也被称为"守护线程"。
前台线程:如果某个线程在执行过程中,能够 阻止进程结束,就被称为"前台线程"
一个线程中可以有多个前台线程(创建的线程默认是前台线程),必须所有的前台线程都结束,进程才能结束
关于线程是否存活:
isAlive()返回值为true表示还存活,false表示线程没了
代码中,new Thread对象的生命周期与内核中实际线程的生命周期不一定是一致的,可能会出现Thread对象存在但是内核中线程不存在的情况,如:
(1)调用start前,此时内核中还未创建线程
(2)线程中的run执行完毕,内核的线程销毁,但此时Thread对象任然存在
注意:不存在Thread对象不存在,而线程还存在的情况
三、线程的状态
线程的状态是⼀个枚举类型 Thread.State,反映了线程生命周期的不同阶段,了解这些更有利于对线程的调试与优化,以下是线程的六种主要状态:
• NEW(初始):
- Thread对象已创建,但是内核的线程还没有(还未调用start方法)
- 安排了⼯作, 还未开始⾏动
• RUNNABLE(可运行):
- 线程的start方法一旦被调用就会进入RUNNABLE状态
- 就绪状态,可能正在CPU上运行,也可能在等待CPU分配资源以便运行
• BLOCKED(阻塞):
- 因为锁竞争而引起的阻塞等待的状态
- 当线程试图获取某一对象的锁,而该对象的锁正被其它线程占有时,就会进入BLOCKED状态
- 一般指线程因同步操作(synchronized)而被阻塞的状态
• WAITING(等待):
- 当线程调用了Object.wait(),Thread.join(),LockSupport.park()等方法时,就会进入WAITING状态
- 此时线程不会争夺CPU资源,一直等待直到其它线程发出对应的通知信息(如notify()或notifyAll())时重新恢复RUNNABLE状态
- 没有超时时间的阻塞等待,如果没有收到通知将会一直等待下去
• TIMED_WAITING(超时等待):
- 与WAITING状态类似,但是是有超时时间的等待
- 当调用Object.wait(long timeout),Thread.join(long),Thread.sleep(long millis),LockSupport.parkNanos(),LockSupport.parkUntil()等方法时,线程会进入TIMED_WAITING状态
- 线程将会等待直到被唤醒或超时
• TERMINATED(终止):
- 当线程的run方法执行完毕 或者由于某些原因(如抛出为捕获的异常 )而提前结束时,线程进入TERMINATED状态
- 当前Thread对象虽然还在,但是内核的线程已经被销毁了(线程已经结束了)
- 终止的线程无法被重启
可以形象的类比于以下状态:
上述线程状态可以通过jdk自带的jconsole来观察:
学习线程的状态,主要就是为了调试与优化,比如遇到某个线程没有正常运行时,就可以观察对应线程的状态,来确定是否是由于一些原因导致线程进入了阻塞状态
四、线程的核心操作
1、启动一个线程-start()
之前我们已经看到了如何通过覆写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。
• 覆写 run ⽅法是提供给线程要做的事情的指令清单
• 线程对象可以认为是把 李四、王五叫过来了
• ⽽调⽤ start() ⽅法,就是喊⼀声:"⾏动起来!",线程才真正独⽴去执⾏了。
调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程
注意要区分好 run与 start的关系:
run :线程的入口,描述了线程 要执行的任务
start :调用了 系统api ,在系统内核中 真正创建了线程(创建PCB加入到链表中)
也就是说,如果直接调用run方法,虽然也能执行run方法内的程序,但也只会在调用它的线程中执行,是不会创建新的线程的,这样就背离多线程编程的初心了
注意
start对于一个Thread对象只能调用一次,java中的Thread对象与内核中的线程是"一对一"的关系,因此,不存在一个线程终止后,再通过调用start重新执行的情况
这是由于一个Thread对象能够对应到多个线程的话,管理起来就会非常麻烦,JVM的设计实现就会非常复杂了。因此java中希望一个Thread对象只能对应到系统中的一个线程,这样在调用start后就可以通过线程的状态来判断是否能成功创建。
如果一个Thread对象是没有调用过start的,此时就会处于NEW状态,此时调用start就可以顺利地执行创建出线程了。而如果已经调用过start,线程就会进入其它状态,只要不是处于NEW状态,接下来执行start都会抛出异常了
2、获取当前线程引用
想在某个线程中,获取到自身的Thread对象的引用,就可以通过currentThread()来获取
任何线程都可以通过这样的操作获取线程的引用
3、休眠当前线程
也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的
java
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
线程执行sleep,就会使这个线程不参与CPU调度,从而把CPU资源让出来,给别人使用。像sleep这样的操作也被称之为"放权",放弃使用CPU的权利
在有些开发场景中,如果某个线程CPU占用率过高,就可以通过sleep来改善。虽然线程的优先级就可以对其产生影响,但影响是比较有限的,通过sleep可以更加明显地影响到CPU占用
4、线程的中断(终止)
在工作中,我们可能会因为领导的一通电话,而不得不停下手头的工作,去做别的事。线程运行时可能也会遇到类似问题,有时我们可能会因为某些原因而需要提前结束线程的运行,该如何停止呢,这就涉及到停止线程的方式了。
如果有两个线程a和b,b正在运行,a想要b结束运行,其实核心 就是a要想办法让b的run方法更快地执行完毕 ,此时b自然就结束了。而不是说b的run执行一半,a直接强行把b结束了。java中结束线程的方法是比较"温柔"的,并不是直接粗暴的。因为如果强制结束某个线程的话,可能导致其逻辑未完全执行,对应的结果数据是个"半成品",从而影响程序最终的结果,这样肯定是不合理的。
1、一个简单的做法是使用自定义的标志位:
java
public class Demo {
//设置全局变量isQuit作为标志位
public static boolean isQuit=false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (!isQuit){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(4000);
System.out.println("main线程尝试终止 t 线程");
//通过修改isQuit变量的值,就能终止 t 线程的执行了
isQuit=true;
}
}
注意,如果把isQuit变成main方法的局部变量,就会出现以下情况:
我们可以看到lambda语句中的isQuit标红报错了。这与lambda表达式/匿名内部类的变量捕获操作有关。
isQuit与lambda表达式定义在一个作用域内 ,lambda的内部是可以访问到lambda外部(与lambda同一作用域 )的变量的。但是lambda的变量是有要求的,能够捕获的变量得是final或者事实final(即虽没有final修饰,但并没有人修改 ),而上述代码中的isQuit变量被修改过,不是final/事实final,导致lambda表达式无法 通过变量捕获操作获取到它,从而导致程序出现了错误
而当把isQuit写成成员变量后,就成了内部类访问外部类的成员变量,这本就是合法的,因此就不会出现问题了
2、使用Thread自带的interrupted作为标志位
Thread中有一个boolean类型的成员变量interrupted,它的初始值为false,表示未被终止,一旦其它线程调用了interrupt方法,就会设置上述标志位值为true,表示已被终止。
java
public class Main {
public static void main(String[] args) throws InterruptedException {
//下列的 lambda 的代码在编译器眼里,出现在 Thread t 的上方
//此时 t 还没有被定义
Thread t=new Thread(()->{
//先获取到线程的引用
Thread currentThread = Thread.currentThread();
//两种判断方式都行
while (!currentThread.isInterrupted()){
//while(!Thread.interrupted())
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
//在主线程中,控制 t 线程被终止,设置上述标志位
t.interrupt();
}
}
但是执行上述代码我们会发现程序运行是有问题的:
可以看出程序运行抛出了一个RuntimeException异常。主要是由于判断isInterrupted()和执行打印操作的速度过快 ,因此在整个循环中,主要时间都是处于sleep状态 中,main调用interrupt()时,不仅仅会设置标志位,还会把sleep给唤醒 ,假如sleep刚执行了100ms,还剩900ms,此时interrupt被调用,sleep就会被强制唤醒,并且抛出interruptedException异常。
又由于catch中的代码默认再次抛出了一个RuntimeException异常,而这个异常没有被处理,就会导致直接抛到JVM层面,使得进程直接异常终止了
这时我们尝试将代码改为:
这样不抛出新的异常而是输出一段语句是否就能让程序正常运行呢?
显然是不能的,我们可以看到在执行catch操作后,线程并没有被终止,仍然在不断地运行输出。
这是由于当sleep等阻塞函数被唤醒后,会清空刚刚设置的interruptded标志位,这样在线程的下次循环判断时,程序就会认为标志位任未被设置,从而继续执行下去了。
此时,如果想要结束循环,结束线程,就需要在catch中加上return/break语句:
这样,线程就能正常被终止了:
出现这样的现象主要是由于java中,线程终止是一个相对"温柔"的过程,并不是强行就终止。当a线程想让b线程终止时,b可以自行决定,是否要终止/是立即还是稍后,这些都由b线程内部的代码来决定,其他线程无权干涉。
比如:
(1)如果b线程想无视a线程的终止请求,就直接在catch中啥也不做,b仍然会继续执行
(2)如果b线程相要立即结束,就在catch中写入return或break,此时b线程就会立刻结束
(3)如果b线程想要稍后结束,就可以在catch上写入一些其他逻辑(如释放资源,清理一些数据,提交一些结果......等收尾工作),这些逻辑完成后,再进行return/break操作
这些全都得益于sleep这类阻塞方法被强制唤醒 时会清除标志位,才能让b做出各种选择,否则b将被强制结束,无法写出让程序继续执行的代码了。这样可以给程序员更多的操作空间
5、线程等待-join()
操作系统针对多个线程的执行,是一个"随机调度",抢占式执行的过程。线程的调度执行是随机的,我们无法确定两个线程的调度顺序,但是可以控制谁先结束谁后结束。
线程等待就是确定两个线程的结束顺序 ,通过让后结束的线程等待先结束的线程执行,进入阻塞 状态,直到先结束的线程执行完毕 ,此时阻塞解除 ,后结束的线程开始执行。这样就能使两个线程的结束顺序得以确定
这时就可以使用join关键字实现线程等待
比如有两个线程a,b,此时在a线程中调用b.join,意思就是让a线程等待b线程先结束,a再继续执行。通俗的来讲,就相当于是让b插入到a线程的执行过程中
不过也要注意a和b本质上还是两个线程,依旧是并发执行,只是确定了结束顺序
代码示例:
java
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
System.out.println("t 线程开始执行");
for (int i = 0; i < 3; i++) {
System.out.println("这是线程 t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t 线程执行结束");
});
t.start();
//让主线程等待 t 线程
System.out.println("main 线程开始等待");
t.join();
System.out.println("main 线程等待结束");
}
}
可以很明显地看到主线程是先开始执行的,但当执行到t.join()语句时,主线程开始阻塞等待 t 线程的执行,当 t 线程执行结束后,join才会返回,主线程才会继续执行后续代码。
不过如果 t 线程先执行完毕了,然后主线程才开始join,此时主线程不会出现阻塞等待,而是会正常执行:
注意
上述操作都是无参数 的join方法,就是"死等",只要被等待的线程没有执行完毕,就会一直阻塞等待。这并不是一个好的选择,因为一旦被等待的线程代码出现bug,可能导致该线程迟迟无法结束,从而使等待线程一直阻塞而无法继续执行其它操作了
方法如下:
可以在join方法中加入参数来确定等待时间,如果等待时间超过设定时间,就会停止等待,退出阻塞状态继续执行后续代码了。
方法汇总:
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊