并发编程笔记

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切换线程,因为切换的时间特别短,所以基本对于⽤户是⽆感知的。

并发编程笔记_第1张图片

2.4应用

同步与异步如何理解?
以调⽤⽅⻆度来讲,如果
需要等待结果返回,才能继续运⾏就是同步
不需要等待结果返回,就能继续运⾏就是异步
注意:同步在多线程中还有另外⼀层意思,是让多个线程步调⼀致

2.4.1异步调⽤:

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

在异步调⽤中,调⽤⽅通常需要提供⼀个回调函数,⽤于接收异步操作的结果。当异步操作完成后,会 直接调⽤
回调函数并将结果传递给它。这样可以让调⽤⽅在异步操作执⾏的过程中继续执⾏其他任务,等到异步 操作完成
后再处理回调结果。这种⽅式可以提⾼程序的并发性和响应速度。
1) 设计
多线程可以让⽅法执⾏变为异步的(即不要巴巴⼲等着)⽐如说读取磁盘⽂件时,假设读取操作花
费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停...
2) 结论
⽐如在项⽬中,视频⽂件需要转换格式等操作⽐较费时,这时开⼀个新线程处理视频转换,避
免阻塞主线程
tomcat 的异步 servlet 也是类似的⽬的,让⽤户线程处理耗时较⻓的操作,避免阻塞
tomcat 的⼯作线程
ui 程序中,开线程进⾏其他操作,避免阻塞 ui 线程

2.4.2多线程提升效率

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

并发编程笔记_第2张图片

如果是串⾏执⾏,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
但如果是四核 cpu,各个核⼼分别使⽤线程 1 执⾏计算 1,线程 2 执⾏计算 2,线程 3 执⾏计算 3, 那么 3 个 线程是并⾏的,花费时间只取决于最⻓的那个线程运⾏的时间,即 11ms 最后加上汇总时间只会花费 12ms
单线程和多线程执⾏⼀个任务,多线程⽐单线程执⾏速度更快,效率更⾼
注意:多核CPU才能提升效率,单核还是要依次执⾏

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

多线程提升效率的原理是通过将⼀个程序分成多个独⽴的线程,同时运⾏这些线程来完成任务。
每个线程都有⾃⼰的代码执⾏路径和堆栈,可以同时运⾏在不同的 CPU 核⼼中。
这样可以最⼤限度地利⽤ CPU 资源,从⽽提⾼程序的执⾏效率。
11 多线程执⾏的原理涉及到并发和并⾏两个概念。
并发是指多个线程交替执⾏,看起来像是同时执⾏,但实际上是在不同的时间⽚中分别执⾏的。
并⾏是指多个线程同时执⾏,利⽤了多核 CPU 的优势,真正意义上的同时执⾏。
多线程提升效率的关键在于任务的分解和协调。
将⼀个⼤任务分解成多个⼩任务,每个⼩任务由⼀个线程独⽴执⾏,最后将所有⼩任务的结果合并起来得到最终 的结果。
线程之间需要进⾏协调和通信,以避免竞争条件、死锁等问题的发⽣
当然,多线程也存在⼀些缺点,⽐如线程之间的协调和通信会增加额外开销,如果线程数量过多也会造成资源争 夺和系统负载过重的问题。因此,在实际应⽤中需要根据具体情况进⾏合理的线程管理和调度,避免出现不必要 的问题。

3.Java线程

3.1.创建和运⾏线程

创建线程

并发编程笔记_第3张图片

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接口,即将任务和线程分离,⽐第⼀种更灵活。
  • 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简化
⼿动简化:

并发编程笔记_第4张图片

⾃动简化:快捷键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 类型的参数,⽤来处理有返回结果的情况

并发编程笔记_第5张图片

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

并发编程笔记_第6张图片

Thread是⼀个类,继承⾃Object类,并且实现了Runnable接⼝,它代表着⼀个线程。
在Thread类中,提供了⼀些⽅法,如start()、join()等,可以控制线程的⽣命周期和执⾏顺序。

再看Runnbale接⼝的源码:

并发编程笔记_第7张图片

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 ⽅法,这才起到多线
程的效果。

并发编程笔记_第8张图片

为什么我们不能直接调⽤ 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 进程中线程的运⾏情况(图形界⾯)

并发编程笔记_第9张图片

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线程有哪些常⽤的调度⽅法?

并发编程笔记_第10张图片

线程等待与通知
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 不同的是,该⽅法如 果发现当前线程被中断,则会清除中断标志

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

并发编程笔记_第11张图片

并发编程笔记_第12张图片

1. start和run

并发编程笔记_第13张图片

调⽤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 ...");
}

并发编程笔记_第14张图片

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

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

并发编程笔记_第15张图片

程序在 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);
}
输出:

并发编程笔记_第16张图片

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

同步怎么理解?
以调⽤⽅⻆度来讲,

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

并发编程笔记_第17张图片

在上⾯的代码中, 主线程同时执⾏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);
}

最后会输出多少?

并发编程笔记_第18张图片

答案是2s

分析如下:
  • 第⼀个 join:等待 t1 时, t2 并没有停⽌, ⽽在运⾏
  • 第⼆个 join:1s 后, 执⾏到此, t2 也运⾏了 1s, 因此也只需再等待 1s

那如果改变两个join的位置呢?
并发编程笔记_第19张图片

答案也是2s

看流程图

并发编程笔记_第20张图片

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());
}

并发编程笔记_第21张图片

总结:
  • 在调⽤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();
 }

并发编程笔记_第22张图片

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();
}

并发编程笔记_第23张图片

打断park线程总结
打断 park 线程, 不会清空打断状态
park只会在打断标记为false时⽣效
没有在sleep调⽤时执⾏打断不需要重置打断标记,因为这时不会清除打断标记

两阶段终⽌模式

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

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

两阶段终⽌模式的实现

并发编程笔记_第24张图片

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();
输出:

并发编程笔记_第25张图片

主线程启动了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();
输出:

并发编程笔记_第26张图片

不推荐使⽤的⽅法

并发编程笔记_第27张图片

3.5主线程与守护线程

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

注意:
垃圾回收器线程就是⼀种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令
后,不会等待它们处理完当前请求

3.6线程有几种状态

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

并发编程笔记_第28张图片

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

并发编程笔记_第29张图片

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

并发编程笔记_第30张图片

【初始状态】仅是在语⾔层⾯创建了线程对象,还未与操作系统线程关联
【可运⾏状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执
⾏【运⾏状态】指获取了 CPU 时间⽚运⾏中的状态
当 CPU 时间⽚⽤完,会从【运⾏状态】转换⾄【可运⾏状态】,会导致线程的上下⽂切换
【阻塞状态】
如果调⽤了阻塞 API,如 BIO 读写⽂件,这时该线程实际不会⽤到 CPU,会导致线程上下⽂
切换,进⼊【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换⾄【可运⾏状态】
与【可运⾏状态】的区别是,对【阻塞状态】的线程来说只要它们⼀直不唤醒,调度器就⼀直
不会考虑调度它们
【终⽌状态】表示线程已经执⾏完毕,⽣命周期已经结束,不会再转换为其它状态

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

并发编程笔记_第31张图片

NEW是线程创建好但还没运⾏
RUNNABLE是运⾏状态
BLOCKED是线程阻塞状态
WAITING是等待状态
TIME_WAITING是超时等待状态
TERMINATED线程终⽌状态

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

并发编程笔记_第32张图片

线程的状态转换图

并发编程笔记_第33张图片

今天先更新到这里。

你可能感兴趣的:(并发编程,并发笔记,多线程)