1、前言

这篇笔记是我花的20多天跟着⿊⻢的并发编程学习做的笔记地址b
⿊⻢ 并发编程
,也是我第⼀次
学习并发
编程,所以如果有很多原理讲不清楚的,知识点不全的,也请多多包涵
中间多数图都是直接截⽼师的笔记代码有时会跟着敲,笔记跟着做的,也有些⾃⼰的想法和总结在⾥ ⾯。

2、进程&线程

2.1进程与线程

辅助解释

进程:
程序指令数据组成,但这些指令要运⾏,数据读写,就必须将指令加载⾄ CPU,数据
加载内存。在指令运⾏过程中还需要⽤到磁盘、⽹络等设备

进程是⽤来加载指令、管理内存管理 IO 的

当⼀个程序被运⾏,从磁盘加载这个程序代码内存,这时就开启了⼀个进程。

进程就可以视为程序的⼀个实例。⼤部分程序可以同时运⾏多个实例进程(例如记事本、画
图、浏览器等),也有的程序只能启动⼀个实例进程(例如⽹易云⾳乐、360 安全卫⼠等)
线程:

⼀个进程之内可以分为⼀到多个线程。

⼀个线程就是⼀个指令流,将指令流中的⼀条条指令以⼀定的顺序交给 CPU 执⾏

Java 中,线程作为最⼩调度单位,进程作为资源分配的最⼩单位

windows 中进程是不活动的,只是作为线程的容器

2.2线程与进程的比较

进程与线程相⽐:

进程基本上相互独⽴的,⽽线程存在于进程中,是进程的⼀个⼦集

进程拥有共享的资源,如内存空间等,供其内部的线程共享

进程间通信较为复杂

同⼀台计算机的进程通信称为 IPC(Interprocess communication

不同计算机之间的进程通信,需要通过⽹络,并遵守共同的协议例如 HTTP

线程通信相对简单,因为它们共享进程内的内存,⼀个例⼦是多个线程可以访问同⼀个共享

线程更轻量,线程上下⽂切换成本⼀般上要⽐进程上下⽂切换低

线程和进程的区别(GPT说)

线程和进程都是计算机中的执⾏单元,但它们之间有⼏个重要的区别:
1.
资源分配⽅式:进程是操作系统进⾏资源分配的基本单位每个进程都有⾃⼰的内存空间、⽂件句柄、⽹络 连接等。⽽线程则是在进程内部创建的,它们共享进程的内存空间和其他资源。
2.
并发性:由于多个线程可以共享进程的内存空间,因此它们可以同时运⾏并相互协作,从⽽实现更⾼的并发 性。⽽进程之间通常需要使⽤进程间通信(IPC)来传递数据同步操作
3.
系统开销:由于进程之间需要独⽴的内存空间和其他资源,所以创建销毁进程时需要较⼤的系统开销。相 ⽐之下,创建和销毁线程的开销要⼩得多,因为它们共享进程的资源。
4.
执⾏顺序:由于线程是在进程内部运⾏的,因此它们的执⾏顺序可能会受到⼀些限制例如,在Java中, 线程的调度通常是由JVM进⾏控制的,⽽且可能⽆法精确控制线程的执⾏顺序。相⽐之下,进程之间的 执⾏顺序通常是可以完全控制的。
总之,线程和进程都是实现并发和并⾏的重要概念。线程通常⽤于执⾏轻量级任务,以提⾼系统的响应能⼒和吞 吐量,⽽进程则更适合执⾏独⽴的、相对较重的任务

2.3、并发并行

单核 cpu 下,线程实际还是 串⾏执⾏ 的。操作系统中有⼀个组件叫做任务调度器,将 cpu 的时
间⽚(windows时间⽚最⼩约为 15 毫秒)分给不同程序使⽤,只是由于 cpu 在线程间(时
间⽚很短)的切换⾮常快,⼈类感觉是 同时运⾏的 。总结为⼀句话就是
微观串⾏,宏观并⾏
, ⼀般会将这种
线程轮流使⽤ CPU 的做法称为并发(concurrent
多核 cpu下,每个core) 都可以调度运⾏线程,这时候线程可以是并⾏的 引⽤ Rob Pike 的⼀段描述

并发(concurrent)是同⼀时间应对(dealing with)多件事情的能⼒

并⾏(parallel)是同⼀时间动⼿做(doing)多件事情的能⼒
操作系统的⾓度来看,线程是
CPU
分配的最⼩单位
  • 并⾏就是同⼀时刻,两个线程都在执⾏。这就要求有两个CPU去分别执⾏两个线程。
  • 并发就是同⼀时刻,只有⼀个执⾏,但是⼀个时间段内,两个线程都执⾏了。并发的实现依赖于 CPU切换线程,因为切换的时间特别短,所以基本对于⽤户是⽆感知的。

2.4应用

以调⽤⽅⻆度来讲,如果

需要等待结果返回,才能继续运⾏就是同步

不需要等待结果返回,就能继续运⾏就是异步
注意:同步多线程中还有另外⼀层意思,是让多个线程步调⼀致

2.4.1异步调⽤:

异步调⽤的核⼼是回调机制,当⼀个异步调⽤发起后,调⽤⽅不必等待结果返回,⽽是可以继续执⾏后 续操作。异 步调⽤会在单独的线程或线程池中执⾏,等到异步调⽤完成后,会通过回调函数结果返回给调⽤⽅。

异步调⽤中,调⽤⽅通常需要提供⼀个回调函数,⽤于接收异步操作结果。当异步操作完成后,会 直接调⽤
回调函数并将结果传递给它。这样可以让调⽤⽅在异步操作执⾏的过程中继续执⾏其他任务,等到异步 操作完成
后再处理回调结果。这种⽅式可以提⾼程序的并发性和响应速度
1) 设计
多线程可以让⽅法执⾏变为异步的(即不要巴巴⼲等着)⽐如说读取磁盘⽂件时,假设读取操作
费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
2) 结论

⽐如在项⽬中,视频⽂件需要转换格式操作⽐较费时,这时开⼀个新线程处理视频转换,避
阻塞主线

tomcat 的异步 servlet 也是类似的⽬的,让⽤户线程处理耗时较⻓的操作,避免阻塞
tomcat 的⼯作线程

ui 程序中,开线程进⾏其他操作,避免阻塞 ui 线程

2.4.2多线程提升效率

充分利⽤多核 cpu 的优势,提⾼运⾏效率。想象下⾯的场景,执⾏ 3 个计算最后计算结果汇总

如果是串⾏执⾏,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
但如果是四核 cpu各个核⼼分别使⽤线程 1 执⾏计算 1,线程 2 执⾏计算 2,线程 3 执⾏计算 3, 那么 3 个 线程是并⾏的,花费时间只取决于最⻓的那个线程运⾏的时间,即 11ms 最后加上汇总时间只会花费 12ms

单线程和多线程执⾏⼀个任务,多线程⽐单线程执⾏速度更快,效率更⾼
注意:多核CPU才能提升效率,单核还是要依次执⾏

多线程提升效率原理怎样的?

多线程提升效率原理通过将⼀个程序分成多个独⽴的线程,同时运⾏这些线程来完成任务
每个线程都有⾃⼰的代码执⾏路径和堆栈,可以同时运⾏在不同的 CPU 核⼼中。
这样可以最⼤限度地利⽤ CPU 资源,从⽽提⾼程序的执⾏效率
11
多线程执⾏的原理涉及到并发和并⾏两个概念

并发是指多个线程交替执⾏,看起来像是同时执⾏,但实际上是在不同的时间⽚中分别执⾏的。

并⾏是指多个线程同时执⾏,利⽤了多核 CPU 的优势,真正意义上的同时执⾏。
多线程提升效率的关键在于任务分解和协调。
将⼀个⼤任务分解成多个⼩任务,每个⼩任务由⼀个线程独⽴执⾏,最后将所有⼩任务的结果合并起来得到最终 的结果
线程之间需要进⾏协调和通信,以避免竞争条件死锁问题的发⽣
当然,多线程也存在⼀些缺点,⽐如线程之间的协调和通信会增加额外开销,如果线程数量过多也会造成资源争 夺和系统负载过重的问题。因此,在实际应⽤中需要根据具体情况进⾏合理的线程管理调度,避免出现不必要 的问题

3.Java线程

3.1.创建和运⾏线程

创建线程

Java程序主⽅法就开启了⼀个线程注意:创建线程的时候最好指定⼀个名字,Thread t1 = new
Thread()把线程和任务(要执⾏的代码)分开

创建线程⽅法1:直接使⽤Thread创建线程对象

public class aa extends Thread {
 public static void main(String[] args) {
 Thread t = new Thread() {
 @Override
 public void run() {
 System.out.println("通过Thread⽅式创建线程");
 }
 };
 t.run();
 //t.start();
 }
}

加星:继承Thead类,重写run方法调用start()方法启动线程。

public class ThreadTest {
 /**
 * 继 承Thread类
 * /
 public static class MyThread extends Thread {
 @Override
 public void run () {
 System . out . println ( "This is child thread" ) ;
 }
 }
 public static void main ( String [] args) {
 MyThread thread = new MyThread ();
 thread .start();
 }
}
创建线程⽅法2:使⽤Runnable配合Thread实现Runnable接口,即将任务和线程分离,⽐第⼀种更灵活。
public class aa extends Thread {
 public static void main(String[] args) {
 Runnable r = new Runnable() {
 @Override
 public void run() {
 System.out.println("使⽤Thread配合Runnable创建线程");
 }
 };
 //创建线程对象
 Thread thread = new Thread(r);
 //启动线程
 thread.start();
 }
}

Lambda简化创建线程:
Tip:接⼝带有@FunctionInterface注解函数式接⼝,就可以使⽤
Lambda简化
⼿动简化

⾃动简化快捷键alt+enter或者alt+shift+enter 提示是否转换成Lambda式;idea会有灰⾊
加星:
实现
Runnable
接⼜,重写
run()
⽅法
public class RunnableTask implements Runnable {
 public void run () {
 System . out . println ( "Runnable!" ) ;
 }
 public static void main ( String [] args) {
 RunnableTask task = new RunnableTask ();
 new Thread ( task ) .start();
 }
}

创建线程⽅法3:FutureTask 配合 Thread(了解)
上⾯两种都是没有返回值的,但是如果我们需要获取线程的执⾏结果,该怎么办呢?

后⾯到线程间通信时再说FutureTask 能够接收 Callable 类型参数,⽤来处理返回结果的情况

Thread与Runnble的关系
分析 Thread 的源码,理清它与 Runnable 的关系
看Thread类的源码


Thread是⼀个类,继承⾃Object类,并且实现了Runnable接⼝,它代表着⼀个线程。

在Thread类中,提供了⼀些⽅法,如start()、join()等,可以控制线程的⽣命周期和执⾏顺序

再看Runnbale接⼝的源码


Runnable是⼀个接⼝,只包含了⼀个run()⽅法,它定义了线程所要执⾏的任务。

Runnable接⼝通常作为参数传递给Thread类的构造函数,让Thread对象来执⾏这个Runnable对象
中的run()⽅法。

这样可以将任务的执⾏和线程的管理分离开来,
提⾼代码的可重⽤性和可维护性
因此,Thread与Runnable之间的关系是:

Thread为Runnable提供了线程的上下⽂环境,具体来说就是调⽤Thread.start()⽅法可以启动⼀个新
线程并执⾏Runnable中的run()⽅法。

通过这种⽅式,可以实现多线程编程,提⾼程序的并发性和效率

同时,将任务和线程分离也符合⾯向对象设计原则中的单⼀职责原则

加星:
实现
Callable
接⼜,重写
call()
⽅法,这种⽅式可以通过
FutureTask
获取任务执⾏的返回值

public class CallerTask implements Callable < String > {
 public String call () throws Exception {
 return "Hello,i am running!" ;
 }

public static void main ( String [] args) {
 / /创建异步任务
 FutureTask < String > task = new FutureTask < String > ( new CallerTask ());
 / /启动线程
 new Thread ( task ) .start();
 try {
 / /等 待 执 ⾏ 完 成 ,并获取返回结果
 String result = task . get();
 System . out . println ( result) ;
 } catch ( InterruptedException e ) {
 e . printStackTrace ();
 } catch ( ExecutionException e ) {
 e . printStackTrace ();
 }
 }
}

思考

为什么调⽤
start()
⽅法时会执⾏
run()
⽅法,那怎么不直接调⽤
run()
法?

JVM
执⾏
start
⽅法,会先创建⼀条线程,由创建出来的新线程去执⾏
thread

run
⽅法,这才起到多线
程的效果

为什么我们不能直接调⽤
run()
⽅法?
也很清楚, 如果直接调⽤
Thread

run()
⽅法,那么
run
⽅法还 是运⾏在主线程中,相当于顺序执⾏,就起不到多线程的效果

观察多个线程同时运⾏
多个线程同时运⾏是指多个线程在同⼀时刻并发地进⾏执⾏。

线程是交替执⾏的

谁先谁后,不由我们控制

需要多核CPU,单核带不动
具体来说,当⼀个程序中有多个线程时,这些线程的启动顺序和执⾏顺序可能是不确定的,每个线程都
有⾃⼰的执⾏路径和执⾏状态,可以在不同的 CPU 核⼼上同时运⾏。
理解多个线程同时运⾏,需要从计算机的硬件操作系统的⻆度来看待。现代计算机通常包含多个 CPU 核⼼或者是⽀持超线程技术的 CPU,这些 CPU 能够并发处理多个指令流。当有多个线程需要执 ⾏时,操作系统会将这些线程分配到不同的 CPU 核⼼或者是时间⽚中,让它们同时运⾏。同时,由于 每个线程都有⾃⼰的代码执⾏路径和堆栈,所以它们之间不会相互⼲扰,可以独⽴地执⾏各⾃的任务。

3.2.查看进程线程的⽅法

Windows系统:

任务管理器可以查看进程和线程数,也可以⽤来杀死进程

tasklist查看进程

taskkill杀死进程

linux系统

ps -ef 查看所有进程

ps -fT -p 查看某个进程(PID)的所有线

kill 杀死进程(kill -9 进程号 强制杀死进程)

top 按⼤写 H 切换是否显示线程

top -H -p 查看某个进程(PID)的所有线

Java程序

jps 命令查看所有 Java 进程

jstack 查看某个 Java 进程(PID)的所有线状态

jconsole 来查看某个 Java 进程中线程的运⾏情况(图形界⾯)

3.3.线程运⾏原理

栈与栈帧

JVM—Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、⽅法区所组成,其中
内存
是给谁⽤的呢?
其实就是
线程
,每个线程启动后,虚拟机就会为其分配⼀块栈内存

Java
虚拟机描述的是
Java
⽅法执⾏的线程内存模型:⽅法执⾏时,
JVM
同步创建⼀个栈帧,⽤来存储局部变量表、操作数栈、动态连接等。


每个栈由多个栈帧(Frame)组成,对应着每次⽅法调⽤时所占⽤的内存

每个线程只能有⼀个活动栈帧,对应当前正在执⾏的那个⽅法

每个栈帧对应⼀个⽅法的执⾏

线程上下⽂切换(Thread Context Switch

线程上下⽂切换:简单来说,
就是CPU不再执⾏当前线程,转⽽执⾏另⼀个线程的代码
下⾯原因会导致线程上下⽂切换

线程的 cpu 时间⽚⽤完

有更⾼优先级的线程需要运⾏

线程⾃⼰调⽤了 sleepyieldwaitjoinparksynchronizedlock 等⽅法

当 线程上下⽂切换Context Switch 发⽣时,需要由
操作系统保存当前线程的状态
,并恢复另⼀ 个线程的状态
Java 中对应概念就是程序计数器(Program Counter Register),它的作⽤是记住下⼀条
jvm 指令的执⾏地址,是线程私有

状态包括程序计数器虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址

上下⽂切换Context Switch 频繁发⽣会影响性能

3.4线程有哪些常⽤的调度⽅法?

线程等待与通知

Object
类中有⼀些函数可以⽤于线程的等待与通知
wait()
:当⼀个线程
A
调⽤⼀个共享变量
wait()
⽅法时, 线程
A
会被阻塞挂起, 发⽣下⾯⼏种
情况才会返回 :

1
) 线程
A
调⽤了共享对象
notify()
或者
notifyAll()
⽅法;

2
)其他线程调⽤了线程
A

interrupt()
⽅法,线程
A
抛出
InterruptedException
异常返 回。
  • wait(long timeout) 这个⽅法相⽐ wait() ⽅法多了⼀个超时参数,它的不同之处在于,如果线 A调⽤共享对象的wait(long timeout)⽅法后,没有指定 timeout ms时间内被其它线程唤 醒,那么这个⽅法还是会因为超时⽽返回。
  • wait(long timeout, int nanos),其内部调⽤的是 wait(long timout函数
  • 上⾯是线程等待的⽅法,⽽唤醒线程主要是下⾯两个⽅法:notify() : ⼀个线程A调⽤共享对象的 notify() ⽅法后,会唤醒⼀个在这个共享变量上调⽤ wait 系列⽅法后被挂起的线程。 ⼀个共享变量可能会有多个线程在等待,具体唤醒哪个等待的线程 随机的。
  • notifyAll() :不同于在共享变量上调⽤ notify() 函数会唤醒被阻塞到该共享变量上的⼀个线程,
  • notifyAll()法则唤醒所有在该共享变量上由于调⽤ wait 系列⽅法⽽被挂起的线程。
  • Thread类也提供了⼀个⽅法⽤于等待的⽅法:
  • join():如果⼀个线程A执⾏了thread.join()语句,其含义是:当前线程A等待thread线程终⽌之
  • 后才 从thread.join()返回。
线程休眠
  • sleep(long millis) :Thread类中静态⽅法,当⼀个执⾏中的线程A调⽤了Thread sleep⽅法 后,线程A会暂时让出指定时间的执⾏权,但是线程A所拥有的监视器资源,⽐如锁还是持有不让 出的。指定的睡眠时间到了后该函数会正常返回,接着参与 CPU 的调度,获取 CPU 资源后就 可以继续运⾏。
让出优先
  • yield() Thread类中静态⽅法,当⼀个线程调⽤ yield ⽅法时,实际就是在暗⽰线程调度器当 前线程请求让出⾃⼰的CPU ,但是线程调度器可以⽆条件忽略这个暗⽰。 
线程中断
Java
中的线程中断是⼀种线程间的协作模式通过设置线程的中断标志并不能直接终⽌该线程的执 ⾏,⽽是被中断的线程根据中断状态⾃⾏处理。
void interrupt()
中断线程,例如,当线程
A
运⾏时,线程
B
可以调⽤线程
interrupt()
⽅法来设
置线程的中断标志为
true
并⽴即返回。设置标志仅仅是设置标志
,
线程
A
实际并没有中断, 会
继续往下执⾏。
线程中常⽤⽅法及功能介绍

1. start和run

调⽤run
public static void main(String[] args) {
 //创建线程
 Thread t1 = new Thread("t1") {
 @Override
 public void run() {
 log.debug(Thread.currentThread().getName());
 FileReader.read(Constants.MP4_FULL_PATH);
 }
 };
 //运⾏线程
 t1.run();
 log.debug("do other things ...");
}

程序仍在 main 线程运⾏, FileReader.read() ⽅法调⽤还是同步

将上⾯代码 t.run(); 改为t.start()

程序在 t1 线程运⾏, FileReader.read() ⽅法调⽤是异步的
run⽅法和start⽅法总结

直接调⽤ run 是在主线程中执⾏了 run,没有启动新的线程

使⽤ start 是启动新的线程,通过新的线程间接执⾏ run 中的代码

需要注意的是,在Java中
不能直接调⽤run()⽅法来启动线程,必须使⽤start()⽅法来启动线程。
start()⽅法会为线程创建⼀个新的执⾏路径,并在该路径上调⽤run()⽅法。
如果直接调⽤run()⽅法,则不会创建新的执⾏路径,⽽是在当前线程上执⾏run()⽅法,这样就失去了 多线程的意义。

2. sleep和yield

Sleep(long n)
让当前线程进⼊休眠,休眠时CPU的时间⽚会让给其他线程

调⽤sleep⽅法会将线程状态由Runnable->Time_Waiting(阻塞状态)

sleep⽅法在哪调⽤就是是哪个线程睡眠(主线程or其他线程)

interrupt⽅法可以打断正在休眠的线程,打断线程后会抛出InterruptedException异常

睡眠结束后的线程未必会得到⽴即执⾏(其他线程在运⾏,CPU时间⽚不会⽴即分给它)

建议使⽤TimeUnit的sleep⽅法代替Thread的sleep⽅法,可读性更好
yield()
提示线程调度器让出当前线程对CPU的使⽤


调⽤yield⽅法后会让线程从Running进⼊Runnable就绪状态,然后调度执⾏其他线程

具体的实现依赖于操作系统的任务调度器

线程优先级

线程优先级会提示(himt)调度器优先调度该线程,但它仅仅是⼀个提示,调度器可以忽略

如果CPU⽐较忙,那么优先级越⾼的线程会获得更多的时间⽚,但CPU闲时,优先级⼏乎没

案例:防⽌CPU占⽤100%

sleep实现

在没⽤利⽤CPU来计算时,不要让whiletrue)空转浪费CPU,这时可以适应yield或sleep来让出
CPU的使⽤权给其他程序

while(true) {
 try {
 Thread.sleep(50);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
}

wait实现
synchronized锁

synchronized(锁对象) {
 while(条件不满⾜) {
 try {
 锁对象.wait();
 } catch(InterruptedException e) {
 e.printStackTrace();
 }
 }
// do sth...
}
条件变量实现
加ReentrantLock
lock.lock();
try {
while(条件不满⾜) {
try {
条件变量.await();//当前线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}

3. join

sleep⽅法可以使线程休眠,那为什么还需要join⽅法
看下⾯这段代码,最终输出的结果会是什么

static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 r = 10;
 });
 t1.start();
 log.debug("结果为:{}", r);
 log.debug("结束");
}
结果是:开始..开始..结果为0..结束..结束…..
为什么会这样呢?明明t1线程给r赋值10了啊,为什么最后打印出来的r不是10?

分析⼀下
  • 主线程和线程 t1 是并⾏执⾏的,t1 线程需要 1 秒之后才能算出 r=10
  • 主线程⼀开始就要打印 r 的结果,所以只能打印出 r=0
解决⽅法
⽤ sleep ⾏不⾏?为什么?
不⾏,因为sleep要设置休眠时间,t1线程具体计算的时间是不确定的,sleep要传的参数也不确定
⽤ join⽅法呢(同步)
t1.join()加在ti.start()之后,就可以解决

join⽅法是让等待当前线程执⾏完才继续向下执⾏

static int result = 0;
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 result = 10;
 }, "t1");
 t1.start();
 t1.join();
 log.debug("结果为:{}", result);
}
输出

t1线程启动后,t1线程调⽤了join⽅法,所以主线程需要等待t1线程执⾏完之后才能执⾏
  • 需要外部共享变量,不符合⾯向对象封装的思想
  • 必须等待线程结束,不能配合线程池使⽤
同步怎么理解
以调⽤⽅⻆度来讲,


需要等待结果返回才能继续运⾏就是同步;

不需要等待结果返回就能继续运⾏就是异步

在上⾯的代码中,
主线程同时执⾏t1线程的运⾏和t1线程的join⽅法就是异步
,调⽤t1⽅法的join⽅法不需 要等待 t1线程执⾏完才能执⾏

主线程后⾯的打印r的值需要等待t1线程执⾏完就是同步

join⽅法例题:
private static void test2() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(1);
 r1 = 10;
 });
 Thread t2 = new Thread(() -> {
 sleep(2);
 r2 = 20;
 });
 t1.start();
 t2.start();
 long start = System.currentTimeMillis();
 log.debug("join begin");
 t1.join();
 log.debug("t1 join end");
 t2.join();
 log.debug("t2 join end");
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

答案是2s

  • 第⼀个 join:等待 t1 时, t2 并没有停⽌, ⽽在运⾏
  • 第⼆个 join:1s 后, 执⾏到此, t2 也运⾏了 1s, 因此也只需再等待 1s
那如果改变两个join的位置呢?

答案也是2s

t1的join⽅法在前⾯时,主线程异步运⾏t1,t2两个线程执⾏,调⽤t1的join⽅法后,t2线程需要等
待t1线
程执⾏完毕,但同时t2线程也在运⾏,t1线程运⾏结束等待t2线程运⾏结束,这时只需要再等待1s
就可,
t2线程已经运⾏了1s,总时间2s
t2的join⽅法在前⾯,同理,调⽤t2的join⽅法,t1线程等待t2线程运⾏结束的同时也在运⾏,所以
t2线程
执⾏完毕后就可以继续向下运⾏,总时间2s

等待多个线程的调度
并⾏执⾏,多个线程会同时运⾏,最终只会消耗时间久的join的时间

有时效的join
线程执⾏会导致join提前结束。
如果开启的t1线程⾥休眠2秒,join1.5秒,那主线程不会等待t1线程执⾏完就会执⾏。
如果开启的t1线程⾥休眠2秒,join3秒,那主线程会随着t1线程执⾏完就提前执⾏(⽆需等join
完)

4.interrupt

打断 sleep,wait,join的线程
,这⼏个⽅法都会使线程进⼊阻塞

1、打断sleep的线程,会清空打断状态

private static void test1() throws InterruptedException {
 Thread t1 = new Thread(()->{
 sleep(1);
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
 log.debug(" 打断状态: {}", t1.isInterrupted());
}

总结
2、打断正常运⾏的线程, 不会清空打断状态

private static void test2() throws InterruptedException {
 Thread t2 = new Thread(()->{
 while(true) {
 Thread current = Thread.currentThread();
 boolean interrupted = current.isInterrupted();
 if(interrupted) {
 log.debug(" 打断状态: {}", interrupted);
 break;
 }
 }
 }, "t2");
 t2.start();
 sleep(0.5);
 t2.interrupt();
 }

3.打断park的线程
private static void test3() {
 Thread t1 = new Thread(() -> {
 log.debug("park...");
 LockSupport.park();
 log.debug("unpark...");
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
}

打断park线程总结

打断 park 线程, 不会清空打断状态

park只会在打断标记false时⽣效

没有在sleep调⽤时执⾏打断不需要重置打断标记,因为这时不会清除打断标记

阶段终⽌模式

阶段终⽌模式(Two-Phase Termination Pattern
是⼀种⽤于在多线程编程中优雅地停⽌线程的模
式。
该模式的主要思想是,在停⽌线程前,先通知线程需要停⽌,并等待其完成完成的⼯作,然后再真正 地停⽌线程。
在⼀个线程T1中如何优雅的终⽌线程T2?这⾥的优雅指的是给T2⼀个料理后事的机会

1.
使⽤线程对象的stop⽅法停⽌线程(stop⽅法会真正杀死线程,如果这时线程锁住了共享资
源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远⽆法获取锁)
2.
使⽤System.exit()⽅法停⽌线程(这个⽅法更暴⼒;⽬的是停⽌⼀个线程,但调⽤这个⽅法会
让整个程序都停⽌)

阶段终⽌模式的实现

1.利⽤interrupt
interrupt 可以打断正在执⾏的线程,⽆论这个线程是在 sleep,wait,还是正常运⾏
模拟打断

class TPTInterrupt {
private Thread thread;
public void start(){

thread = new Thread(() -> {

while(true) {

Thread current = Thread.currentThread();

if(current.isInterrupted()) {

log.debug("料理后事");

break;

}

try {

Thread.sleep(1000);

log.debug("将结果保存");
} catch (InterruptedException e) {

current.interrupt();
}
// 执⾏监控操作
}
},"监控线程");
thread.start();
}

public void stop() {

thread.interrupt();
}
}
调⽤:

TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
输出

主线程启动了t线程后进⼊休眠,在这期间t线程⽆法“料理后事”,每隔⼀秒将结果保存,主线程休眠结束
后调⽤
stop⽅法,打断t线程,结束

2.利⽤打断标记

设置停⽌标记stop,⽤volatile修饰,保证其在多线程间的可⻅性

// 停⽌标记⽤ volatile 是为了保证该变量在多个线程之间的可⻅性
// 我们的例⼦中,即主线程把它修改true 对 t1 线程可⻅
class TPTVolatile {
 private Thread thread;
 private volatile boolean stop = false;
 public void start(){
 thread = new Thread(() -> {
 while(true) {
 Thread current = Thread.currentThread();
 if(stop) {
 log.debug("料理后事");
 break;
 }
 try {
 Thread.sleep(1000);
 log.debug("将结果保存");
 } catch (InterruptedException e) {
 
 }
 // 执⾏监控操作
 }
 },"监控线程");
 thread.start();
 }
 
 public void stop() {
 stop = true;
 thread.interrupt();
 }
}

调⽤:
TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
输出

推荐使⽤的⽅法

3.5主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运⾏结束,才会结束。
有⼀种特殊的线程叫做守护线程,只要其它⾮守护线程运⾏结束了,即使守护线程的代码没有执⾏
完,也会强制结束。

注意:

垃圾回收器线程就是⼀种守护线程

Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat收到 shutdown 命令
后,不会等待它们处理完当前请求

3.6线程有几种状态


Java
中,线程共有六种状态:

线程在⾃⾝的⽣命周期中, 并不是固定地处于某个状态,⽽是随着代码的执⾏在不同的状态之间进⾏
切换,
Java
线程状态变化如图⽰:

操作系统层⾯描述,线程有5种状态

初始状态】仅是在语⾔层⾯创建了线程对象,还未与操作系统线程关联
【可运⾏状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执
⾏【运⾏状态】指获取了 CPU 时间⽚运⾏中的状态

当 CPU 时间⽚⽤完,会从【运⾏状态】转换⾄【可运⾏状态】,会导致线程的上下⽂切换
【阻塞状态】

如果调⽤了阻塞 API,如 BIO 读写⽂件,这时该线程实际不会⽤到 CPU,会导致线程上下⽂
切换,进⼊【阻塞状态】

等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换⾄【可运⾏状态】

与【可运⾏状态】的区别是,对【阻塞状态】的线程来说只要它们⼀直不唤醒,调度器就⼀直
不会考虑调度它们
【终⽌状态】表示线程已经执⾏完毕,⽣命周期已经结束,不会再转换为其它状态

线程的六种状态
从Java API层⾯描述,线程有6种状态


NEW是线程创建好但还没运⾏

RUNNABLE是运⾏状态

BLOCKED是线程阻塞状态

WAITING是等待状态

TIME_WAITING是超时等待状态

TERMINATED线程终⽌状态

线程的六种状态状态(⼆哥)

线程的状态转换

今天更新这里

原文地址:https://blog.csdn.net/qq_64948664/article/details/134753721

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_38722.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注