💐个人主页:初晴~
上篇文章我们简单介绍了什么是进程与线程,以及他们之间的区别与联系,实际应用中还是以多线程编程为主的,所以这篇文章就让我们更加深入地去剖析多线程编程的具体应用吧
目录
一、初识Thread类
1、创建线程
(1)继承Thread类
编写的MyThread需要继承Thread类,不需要导包,因为Thread类是java.lang中内置的类。
继承不是主要目的,主要是为了重写Thread类中的run方法,在其中写入所创建线程需要执行的逻辑语句。
若要让线程运行,需先实例化编写的MyThread类,接着调用start方法就会在进程内部创建一个新的线程,新的线程就会执行刚才run里的代码。
具体代码如下:
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();
}
}
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);
}
}
}
}
注意:
上述代码中,并没有直接手动调用run方法,但是也被执行了。像run这种,用户手动定义了,但是没有手动调用,最终被系统/库/框架调用执行了的方法,被称为“回调函数(call back)”。
(2)实现Runnable接口
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 ⼦类对象
Thread t=new Thread(){
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
1、定义匿名内部类,这个类是Thread的子类2、类的内部,重写了父类的run方法3、创建了一个子类的实例,并把实例的引用赋值给了t
//使⽤匿名类创建 Runnable ⼦类对象
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
lambda表达式创建 Runnable 子类对象:
Thread t3=new Thread(()-> System.out.println("使⽤lambda表达式创建 Runnable ⼦类对象"));
Thread t4=new Thread(()->{
System.out.println("使用lambda表达式创建 Runnable 子类对象");
});
lambda表达式本质上就是匿名内部类的更简化的写法,很多时候,写匿名内部类都不是为了写“类”,而是写类中的“方法”,而lambda就可以避开类而直接描述其中的run方法
2、多线程的优势
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 类及常⻅⽅法
1、 Thread 的常⻅构造⽅法
![](https://i-blog.csdnimg.cn/direct/35b98b0a7aab4d60a7149a4e2f3c08ff.png)
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
如果不给线程起名字,那么默认就会是叫做Thread-0,Thread-1……
给线程起名字,并不会影响线程的执行效果,不过起一个合适的名字,更有利于调试程序
2、 Thread 的⼏个常⻅属性
![](https://i-blog.csdnimg.cn/direct/878f9d5c275548e7921aef0b4c24a50a.png)
• ID 是JVM自动分配的,是线程的唯⼀标识,不同线程不会重复• 名称是各种调试⼯具⽤的,Thread对象的身份标识• 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明• 优先级⾼的线程理论上来说更容易被调度到• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。• 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了• 线程的中断问题,下⾯我们进⼀步说明
关于前台线程与后台线程:
后台线程:如果这个线程执行过程中,不能阻止进程的结束,(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就被称为“后台线程”,也被称为“守护线程”。
前台线程:如果某个线程在执行过程中,能够阻止进程结束,就被称为“前台线程”
一个线程中可以有多个前台线程(创建的线程默认是前台线程),必须所有的前台线程都结束,进程才能结束
关于线程是否存活:
isAlive()返回值为true表示还存活,false表示线程没了
代码中,new Thread对象的生命周期与内核中实际线程的生命周期不一定是一致的,可能会出现Thread对象存在但是内核中线程不存在的情况,如:
(1)调用start前,此时内核中还未创建线程
(2)线程中的run执行完毕,内核的线程销毁,但此时Thread对象任然存在
注意:不存在Thread对象不存在,而线程还存在的情况
三、线程的状态
• 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与 start的关系:run:线程的入口,描述了线程 要执行的任务start:调用了 系统api,在系统内核中 真正创建了线程(创建PCB加入到链表中)
注意start对于一个Thread对象只能调用一次,java中的Thread对象与内核中的线程是“一对一”的关系,因此,不存在一个线程终止后,再通过调用start重新执行的情况
![](https://i-blog.csdnimg.cn/direct/5592c0808f864b239f27a8a14154c7df.png)
2、获取当前线程引用
想在某个线程中,获取到自身的Thread对象的引用,就可以通过currentThread()来获取
任何线程都可以通过这样的操作获取线程的引用
3、休眠当前线程
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、一个简单的做法是使用自定义的标志位:
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,表示已被终止。
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本质上还是两个线程,依旧是并发执行,只是确定了结束顺序
代码示例:
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方法中加入参数来确定等待时间,如果等待时间超过设定时间,就会停止等待,退出阻塞状态继续执行后续代码了。
方法汇总:
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊
![](https://i-blog.csdnimg.cn/direct/8f268c4eed2243bd91d841cde63179ee.png)