并发编程笔记

1、前言

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

2、进程&线程

2.1进程与线程

  • 进程:进程是代码在数据集合上的一次运行活动,是系统资源分配和调度的基本单位。
  • 线程:线城是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

辅助解释:

进程:
程序由指令和数据组成,但这些指令要运⾏,数据要读写,就必须将指令加载⾄ CPU,数据
加载⾄内存。在指令运⾏过程中还需要⽤到磁盘、⽹络等设备。
进程是⽤来加载指令、管理内存、管理 IO 的
当⼀个程序被运⾏,从磁盘加载这个程序的代码⾄内存,这时就开启了⼀个进程。
进程就可以视为程序的⼀个实例。⼤部分程序可以同时运⾏多个实例进程(例如记事本、画
图、浏览器等),也有的程序只能启动⼀个实例进程(例如⽹易云⾳乐、360 安全卫⼠等)

线程:
⼀个进程之内可以分为⼀到多个线程。
⼀个线程就是⼀个指令流,将指令流中的⼀条条指令以⼀定的顺序交给 CPU 执⾏
Java 中,线程作为最⼩调度单位,进程作为资源分配的最⼩单位。
在 windows 中进程是不活动的,只是作为线程的容器

2.2线程与进程的比较

进程与线程相⽐:
进程基本上相互独⽴的,⽽线程存在于进程中,是进程的⼀个⼦集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同⼀台计算机的进程通信称为 IPC(Inter-process 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() {@Overridepublic void run() {System.out.println("通过Thread⽅式创建线程");}};t.run();//t.start();}
}

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

public class ThreadTest {/*** 继 承Thread类* /public static class MyThread extends Thread {@Overridepublic 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接口,即将任务和线程分离,⽐第⼀种更灵活。
  • Thread 代表线程
  • Runnable代表可运⾏的任务(线程要执⾏的任务)

public class aa extends Thread {public static void main(String[] args) {Runnable r = new Runnable() {@Overridepublic 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 时间⽚⽤完
垃圾回收
有更⾼优先级的线程需要运⾏
线程⾃⼰调⽤了 sleep、yield、wait、join、park、synchronized、lock 等⽅法

当 线程上下⽂切换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 实际并没有被中断, 会
继续往下执⾏。
  • boolean isInterrupted() ⽅法: 检测当前线程是否被中断。
  • boolean interrupted() ⽅法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该⽅法如 果发现当前线程被中断,则会清除中断标志

线程中常⽤⽅法及功能介绍

1. start和run

调⽤run
public static void main(String[] args) {//创建线程Thread t1 = new Thread("t1") {@Overridepublic 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来计算时,不要让while(true)空转浪费CPU,这时可以适应yield或sleep来让出
CPU的使⽤权给其他程序

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

  • 可以⽤wait或条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要响应的唤醒操作,⼀般适⽤于要进⾏同步的场景
  • sleep适⽤于⽆需锁同步的场景
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());
}

总结:
  • 在调⽤sleep时执⾏打断会出现异常;
  • sleep在调⽤时会清除打断标记
  • 异常被caych了不会终⽌程序
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线程终⽌状态

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

线程的状态转换图

今天先更新到这里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/195205.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Java SSM框架+Vue实现疫情期间医院门诊网站项目【项目源码+论文说明】

基于java的SSM框架Vue实现疫情期间医院门诊网站演示 摘要 21世纪的到来&#xff0c;国家的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;智能科技时代崛起的优势&#xff0c;医院门诊管理系统当然也不能排除在外。疫情期间医院门诊管理系统是以实际运用为开发背…

【JVM系列】Class文件分析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

终端安全管理软件是监控软件吗

有些人在后台问&#xff0c;终端安全管理软件是监控软件吗&#xff1f; 先回答&#xff0c;是监控软件。 因为它具有监控的功能&#xff0c;在很大程度上&#xff0c;是可以用来当做监控软件来用的。 终端安全管理软件是一种集中管理终端设备的软件工具&#xff0c;可以在企业…

猜数字赢金币

充值金币后开始游戏&#xff0c;猜中奖励10金币退出&#xff0c;不中扣除1金币继续。 (笔记模板由python脚本于2023年12月03日 21:52:23创建&#xff0c;本篇笔记适合熟悉程序函数式编程&#xff0c;熟练掌握基本数据类型的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&…

linux常用命令-find命令与scp命令详解(超详细)

文章目录 前言一、find命令介绍1. find命令简介2. find命令的基本语法3. 常用的find命令选项和表达式 二、find命令示例用法1. 按照名称进行搜索2. 按照类型进行搜索3. 按照修改时间进行搜索4. 按照文件大小进行搜索5. 对搜索到的文件执行指定的命令6. 删除搜索到的文件 三、sc…

线程池怎么用?---实例讲解

线程池使用实例 先写一个配置类 /*** 线程池配置*/ Configuration public class ThreadPoolConfig {//定义线程前缀public static final String NAME_PRE"test";/*** ExecutorService 这个对象就是线程池&#xff0c;可以点进去他的源码看看* Bean&#xff0c;将ge…

2024年甘肃省职业院校技能大赛(中职教师组)网络安全竞赛样题卷③

2024年甘肃省职业院校技能大赛&#xff08;中职教师组&#xff09;网络安全竞赛样题卷③ 2024年甘肃省职业院校技能大赛&#xff08;中职教师组&#xff09;网络安全竞赛样题卷③A模块基础设施设置/安全加固&#xff08;200分&#xff09;A-1任务一 登录安全加固&#xff08;Wi…

400页Python学习PDF笔记,全面总结零基础入门看这一篇足够了

我们都知道Python入门比较简单&#xff0c;但仍有很多想要学习的新手依然卡在基础的安装阶段&#xff0c;尽管如此&#xff0c;网络上的大部分的教程却对这些基础内容都是一带而过&#xff0c;导致许多新手朋友对这些基础知识一知半解&#xff0c;往往一个小问题都需要在网上查…

电脑回收站还原的文件在哪里找到?如何找回回收站还原的文件

电脑回收站是一种非常有用的功能&#xff0c;可以帮助我们恢复无意中删除的文件。然而&#xff0c;许多人可能不清楚还原的文件在哪里可以找到。本文将为您带来详细解答&#xff0c;并帮助您找回回收站还原的文件。 电脑回收站还原的文件在哪里找到 当我们使用电脑的回收站功…

element的el-date-picker时间控件,限制选择范围区间天数并且当前之后的日期不可选

element的el-date-picker时间控件&#xff0c;限制选择范围区间天数并且当前之后的日期不可选 HTML部分代码 <el-date-pickerv-model"dateRange"type"datetimerange"value-format"yyyy-MM-dd HH:mm:ss"range-separator"至"start-p…

element 弹窗在弹出后鼠标还可以点击页面其他元素

文章目录 需求分析需求 如下图所示,在点击弹出弹框后,支持 鼠标可点击弹框外的其他地方可拖拽弹框弹出弹出后不可有遮挡弹出样式可自定义 分析 官网:https://vxetable.cn/v4/#/table/start/install 安装 vxe-table 引入import {App, createApp }

速达软件全系产品任意文件上传漏洞复现 [附POC]

文章目录 速达软件全系产品任意文件上传漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 速达软件全系产品任意文件上传漏洞复现 [附POC] 0x01 前言 免责声明&#xff1a;请勿利用文章内的相关技…

centos安装Python3之后yum不能使用异常

场景&#xff1a; 需要在centos上安装Python3&#xff0c;但是安装Python3之后出现yum不能使用的问题。 问题描述 在centos上安装python3之后出现yum使用不了问题&#xff0c;使用yum会报如下信息&#xff1a; [roothadoop101~]# yum install wgetFile "/usr/bin/yum&q…

旋转框(obb)目标检测计算iou的方法

首先先定义一组多边形&#xff0c;这里的数据来自前后帧的检测结果 pre [[[860.0, 374.0], [823.38, 435.23], [716.38, 371.23], [753.0, 310.0]],[[829.0, 465.0], [826.22, 544.01], [684.0, 539.0], [686.78, 459.99]],[[885.72, 574.95], [891.0, 648.0], [725.0, 660.0]…

Matlab论文插图绘制模板第129期—函数网格曲面图

在之前的文章中&#xff0c;分享了Matlab函数折线图的绘制模板&#xff1a; 函数三维折线图&#xff1a; 进一步&#xff0c;再来分享一下函数网格曲面图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自…

activemq启动成功但web管理页面却无法访问

前提&#xff1a; 在linux启动activemq成功&#xff01;本地能ping通linux 处理方案&#xff1a; 确定防火墙是否关闭&#xff0c; 有两种处理方案&#xff1a;第一种-关闭防火墙&#xff1b;第二种-暴漏8161和61616两个端口 netstat -lnpt查看8161和61616端口 注意&#xf…

网络细节核心笔记

来源&#xff0c;做个笔记&#xff0c;讲的还蛮清楚通信原理-2.5 数据封装与传输05_哔哩哔哩_bilibili 交换机

慈善始于心,行善贵有恒 | 中创开展“寒冬送温暖”公益活动

岁暮隆冬&#xff0c;冷霜挂睫&#xff0c;前往尖山村的路上雾气弥漫&#xff0c;弯弯绕绕的山路需要开车一小时才能到达目的地。对许多人来说&#xff0c;这或许是一段漫长而艰辛的路程&#xff0c;但对于那些生活在山区的贫困儿童而言&#xff0c;这条山路却是通往外界的唯一…

IEEE 机器人最优控制开源库 Model-based Optimization for Robotics

系列文章目录 文章目录 系列文章目录前言一、开源的库和工具箱1.1 ACADO1.2 CasADi1.3 Control Toolbox1.4 Crocoddyl1.5 Ipopt1.6 Manopt1.7 LexLS1.8 NLOpt1.9 qpOASES1.10 qpSWIFT1.11 Roboptim 二、其他库和工具箱2.1 MUSCOD2.2 OCPID-DAE12.3 SNOPT 前言 机器人&#xff…

python爬虫基础html内容解析库BeautifulSoup

我们通过Requests请求url获取数据&#xff0c;请求把数据返回来之后就要提取目标数据&#xff0c;不同的网站返回的内容通常有多种不同的格式&#xff0c;一种是 json 格式&#xff0c;我们可以直接通过json.loads转换python的json对象处理。另一种 XML 格式的&#xff0c;还有…