并发编程笔记

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.rhkb.cn/news/209512.html

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

相关文章

Fiddler抓包工具之fiddler的composer可以简单发送http协议的请求

一&#xff0c;composer的详解 右侧Composer区域&#xff0c;是测试接口的界面&#xff1a; 相关说明&#xff1a; 1.请求方式&#xff1a;点开可以勾选请求协议是get、post等 2.url地址栏&#xff1a;输入请求的url地址 3.请求头&#xff1a;第三块区域可以输入请求头信息…

全球与中国工业冰箱市场:增长趋势、竞争格局与前景展望

工业制冷机是用来维持储运容器内低温冷藏环境&#xff0c;以防止食品饮料、药品、化学品等对温度敏感的产品腐败变质的系统。此外&#xff0c;冷冻机、热交换器等冷冻系统也用于在工业机械运作过程中保持冷却。工业冷冻系统的需求成长主要是由食品和饮料产业的成长所推动的。 冷…

蓝桥杯每日一题2023.12.3

题目描述 1.移动距离 - 蓝桥云课 (lanqiao.cn) 题目分析 对于此题需要对行列的关系进行一定的探究&#xff0c;所求实际上为曼哈顿距离&#xff0c;只需要两个行列的绝对值想加即可&#xff0c;预处理使下标从0开始可以更加明确之间的关系&#xff0c;奇数行时这一行的数字需…

排序算法介绍(一)插入排序

0. 简介 插入排序&#xff08;Insertion Sort&#xff09; 是一种简单直观的排序算法&#xff0c;它的工作原理是通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。插入排序在实现上&#xff0c;通常…

计算机组成学习-指令系统总结

复习本章时&#xff0c;思考以下问题&#xff1a; 1)什么是指令&#xff1f;什么是指令系统&#xff1f;为什么要引入指令系统&#xff1f;2)一般来说&#xff0c;指令分为哪些部分&#xff1f;每部分有什么用处&#xff1f;3)对于一个指令系统来说&#xff0c;寻址方式多和少…

龙芯loongarch64服务器编译安装maturin

前言 maturin 是一个构建和发布基于 Rust 的 Python 包的工具,但是在安装maturin的时候,会出现如下报错:error: cant find Rust compiler 这里记录问题解决过程中遇到的问题: 1、根据错误的提示需要安装Rust Compiler,首先去其官网按照官网给的解决办法提供进行安装 curl…

SocialSelling社交销售1+5+1方法论系列:社交销售价值何在

社交销售作为差异化的社媒社交营销模式&#xff0c;在实际运营操作过程中需要实现个人、部门与系统的融合打磨&#xff0c;并非只是应用工具那么简单。为了更高效地服务赋能客户&#xff0c;傲途基于实战提炼出社交销售151方法论&#xff0c;在近期系列内容中&#xff0c;我们将…

浅谈安科瑞无线测温设备在挪威某项目的应用

摘要&#xff1a;安科瑞无线温度设备装置通过无线温度收发器和各无线温度传感器直接进行温度值的传输&#xff0c;并采用液晶显示各无线温度传感器所测温度。 Absrtact:Acre wireless temperature device directly transmits the temperature value through the wireless temp…

鸿蒙4.0开发笔记之ArkTS装饰器语法基础@Prop@Link@State状态装饰器(十二)

文章目录 一、哪些是状态装饰器二、StatePropLink状态传递的核心规则三、状态装饰器练习 一、哪些是状态装饰器 1、State&#xff1a;被装饰拥有其所属组件的状态&#xff0c;可以作为其子组件单向和双向同步的数据源。当其数值改变时&#xff0c;会引起相关组件的渲染刷新。 …

力扣116. 填充每个节点的下一个右侧节点指针(详细讲解root根节点的理解)

题目&#xff1a; 给定一个 完美二叉树 &#xff0c;其所有叶子节点都在同一层&#xff0c;每个父节点都有两个子节点。二叉树定义如下&#xff1a; struct Node {int val;Node *left;Node *right;Node *next; } 填充它的每个 next 指针&#xff0c;让这个指针指向其下一个右…

LLM之RAG实战(一):使用Mistral-7b, LangChain, ChromaDB搭建自己的WEB聊天界面

一、RAG介绍 如何使用没有被LLM训练过的数据来提高LLM性能&#xff1f;检索增强生成&#xff08;RAG&#xff09;是未来的发展方向&#xff0c;下面将解释一下它的含义和实际工作原理。 ​ 假设您有自己的数据集&#xff0c;例如来自公司的文本文档。如何让ChatGPT和其他…

ZPLPrinter Emulator SDK for .NET 6.0.23.1123​ Crack

ZPLPrinter Emulator SDK for .NET 适用于 .NET 的 ZPLPrinter 仿真器 SDK 允许您通过编写 C# 或VB.NET 代码针对任何 .NET Framework、.NET CORE、旧版 ASP.NET MVC 和 CORE、Xamarin、Mono 和通用 Windows 平台 (UWP) 作业。 适用于 .NET 的 ZPLPrinter 仿真器 SDK 允许您将…

matplotlib与opencv图像读取与显示的问题

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 最近在用opencv和matplotlib展示图片,但是遇到了一些问题,这里展开说说 首先需要明确的是,opencv和matplotlib读取图片都是通道在最后,而前者默认可见光图像是BGR,后者是RGB.此外还有PIL以及imageio等读取图像的工具…

(学习笔记)Xposed模块编写(一)

前提&#xff1a;需要已经安装Xposed Installer 1. 新建一个AS项目 并把MainActvity和activity_main.xml这两个文件删掉&#xff0c;然后在AndriodManifest.xml中去掉这个Activity的声明 2. 在settings.gralde文件中加上阿里云的仓库地址&#xff0c;否则Xposed依赖无法下载 m…

9款热门API接口分享,值得收藏!

电商API接口 干货分享 开始 “ API是什么&#xff1f; API的主要目的是提供应用程序与开发人员以访问一组例程的能力&#xff0c;而又无需访问源码&#xff0c;或理解内部工作机制的细节。提供API所定义的功能的软件称作此API的实现。API是一种接口&#xff0c;故而是一种抽象…

Banana Pi最新的路由器板BPI-R4上市销售,基于MediaTek MT7988A

Banana Pi 发布了一款新的路由器板 Banana Pi BPI-R4&#xff0c;基于配备四核 Arm CPU 的 MediaTek MT7988A SoC。该板不仅仅是Raspberry Pi 的另一个替代品&#xff0c;而且是用于家庭网络和自动化的设备。 Banana Pi BPI-R4 的外形尺寸比单板计算机更像网络设备。对于那些希…

Python函数的基本使用(一)

Python函数的基本使用&#xff08;一&#xff09; 一、函数概述二、函数的定义2.1 函数的语法2.2 语法说明2.3 函数定义的方式2.4 总结 三、函数的调用3.1 函数调用语法3.2 语法说明3.3 函数调用 四、函数的参数4.1 参数的分类4.2 必需参数4.3 默认值参数4.4 关键字参数4.5 不定…

风变科技千万营收的AIGC项目,在Fanbook成功落地,专访风变科技CMO江育麟

在AIGC时代&#xff0c;创作生产力被下放到了每一位普通人身上&#xff0c;然后用户与AIGC应用之间还存在一定的认知与技术沟壑。 最近&#xff0c;【AIGC开放社区】注意到一款AIGC课程项目受到了相当的关注&#xff0c;让许多0基础的学员轻松地学会了使用AIGC技术的基本方法。…

风控交易系统跟单系统资管软件都有哪些功能特点?

资管分仓软件的主要功能就是母账户可以添加子账号&#xff0c;并且设置出入金&#xff0c;手续费、保证金、风控等功能&#xff0c;同时监控端更可以直观的看子账户的交易情况直接折线图展示更加直观&#xff0c;在监控端的最高权限可以直接一键平仓子账户&#xff08;如果子账…

基于jsp的搜索引擎

摘 要 随着互联网的不断发展和日益普及&#xff0c;网上的信息量在迅速地增长&#xff0c;在2004年4月&#xff0c;全球Web页面的数目已经超过40亿&#xff0c;中国的网页数估计也超过了3亿。 目前人们从网上获得信息的主要工具是浏览器&#xff0c;搜索引擎在网络中占有举足轻…