导图社区 多线程并发
多线程并发是现代编程中不可避免的主题之一,它可以提高程序的执行效率并增强程序的响应能力。为了帮助大家更好地理解多线程并发,我做了一份详细的思维导图,它涵盖了线程的基础概念、线程的创建和运行、线程的同步和互斥、锁的种类和使用方法、线程池的实现和使用、多线程调试和优化等重要内容。这份思维导图可以帮助你全面了解多线程并发编程,从而帮助你写出更高效、更健壮的程序。无论你是初学者还是有经验的开发人员,这份思维导图都将为你提供有用的帮助。
编辑于2023-02-14 16:02:12 四川省这份思维导图主要按照《python从入门到实践》的大纲来做出来的,并在相关内容的解释处加入了相关代码,欢迎大家一起学习!
职能地图-Java,干货分享~Java语言技术,java技术扩展,数据结构,维优,个人职能,技术面试知识点总结。
当今大型软件系统的开发多采用企业级的开发模式,而Java语言也是目前较为流行的企业级开发语言之一。针对Java企业级开发,涉及的知识点和技术栈较为丰富,包括但不限于Java EE、Spring框架、Hibernate框架、Maven、Git、Jenkins等等。这份思维导图以Java企业级开发为主题,通过图解的形式将涉及的知识点进行了梳理和整理,从Java EE体系结构、Servlet、JSP、Spring框架、Hibernate框架、Maven等基础知识开始讲解,逐步深入到SpringMVC、
社区模板帮助中心,点此进入>>
这份思维导图主要按照《python从入门到实践》的大纲来做出来的,并在相关内容的解释处加入了相关代码,欢迎大家一起学习!
职能地图-Java,干货分享~Java语言技术,java技术扩展,数据结构,维优,个人职能,技术面试知识点总结。
当今大型软件系统的开发多采用企业级的开发模式,而Java语言也是目前较为流行的企业级开发语言之一。针对Java企业级开发,涉及的知识点和技术栈较为丰富,包括但不限于Java EE、Spring框架、Hibernate框架、Maven、Git、Jenkins等等。这份思维导图以Java企业级开发为主题,通过图解的形式将涉及的知识点进行了梳理和整理,从Java EE体系结构、Servlet、JSP、Spring框架、Hibernate框架、Maven等基础知识开始讲解,逐步深入到SpringMVC、
多线程并发
线程
程序执行的最小单元
创建过程
实现 Runable
实现 Callable : 会得到一个 Feature 的返回
Feature.get 会阻塞线程
get 使用了 LockSupport.parkNanos 有限期等待
LockSupport.unpark(Thread) 才能唤醒
继承 Thread
启动
start
中断
interrupted
判断当前线程是否中断了,调用后会重置状态
如果一个线程 run() 执行了一个无限循环并且没有 sleep 等可以响应 InterruptedException 的方法那么就无法使用 interrupt() 中断此线程
可以在死循环内部判断 interrupted 状态,如果为 true 主动退出
isInterrupted
判断线程是否中断了,不会重置状态
interrupt
中断线程
如果该线程处于阻塞、期限等待或无期限等待,那就就会抛出 interruptedException,从而提前结束线程
但是不能停止 IO 阻塞和 sync 阻塞
不可重新开启会报错
InterruptedException
线程状态
New
Runnable:包含两个子状态
Ready:就绪状态,放入了等待队列汇总等待执行
Running:执行,真的在 CPU 上执行呢
TimeWaiting:有限期等待
Thread.sleep(time)
Object.wait(time)
Thread.join(time)
LockSupport.parkNanos
调用 LocakSupport.unpark(Thread) 退出
LockSupport.parkUntial
调用 LocakSupport.unpark(Thread) 退出
Waiting:无限期等待
Object.wait
调用 Object.notify / Object.notifyAll 退出
Thread.join
被调用的线程执行完毕
LockSupport.park
调用 LocakSupport.unpark(Thread) 退出
Blocked
Teminated
结束状态死亡
可能是自己运行完结束了
也可能是产生了异常结束了
互斥同步
synchronized 关键字
可以指定任意对象为锁定目标
锁的是对象,不是代码!对象头的两位:markwork来标记锁
作为 Monitor 的对象一定要声明为 final 否则在 Monitor 重新赋值后线程将乱套
不能使用 String 常量、Integer、Long 等基础类型的封装类
锁的范围
synchronized(this)\ synchronized void m(){} 都是锁定当前类
synchronized(T.class)\ synchronzid static void m(){}锁定字节码对应的 .class 对象
自动释放
异常:如果程序出现异常 synchronized 持有的锁将自动释放,如果不想释放需要 try catch
程序运行结束
可重入
同一个线程可以多次访问同一个锁,方位一次数量 + 1,释放一个数量 -1
synchronized 必须是重入的。例如类 A 的方法 synchronized a,B 继承了A 重写 synchronized a` 并调用了 super.a。那么如果 synchronized 不是可重入的则继承关系就造成了死锁
底层实现
JDK 5 之前是重量级的,都得找 OS 申请锁效率低
JDK 1.6 之后进行了优化性能追平了 ReentrantLock 所以在 JDK 1.8 的时候 ConcurrentHashMap 再次采用了 synchronized
优化:锁升级
锁只能升级不能降级
例如 sync(object)
偏向锁:只在 object 对象头的 markwrd 记录线程 ID,如果访问者都是同一个线程则放行
没有额外消耗和非同步的性能相近
适合基本没有竞争的同步场景
自旋锁(CAS):如果线程争用,升级为自旋锁。就是定时查看锁是否释放
消耗 CPU 但是不访问内核,在用户态解决竞争问题,效率要比经过内核态的效率高
适用于竞争小线程数量少、且线程持有所得时间不长,追求响应快的场景
默认自旋10次
重量级锁
阻塞线程,有 OS 提供支持,需要内核态参与
适合任务的执行时间长、任务少的场景
ReentrantLock
特征
需要手动释放锁,更灵活,也更危险
一定要在 finally 中释放锁,不然很容易死锁
公平性
ReentrantLock 的构造函数中可以指定是否为公平锁,模式是公平的
公平锁:执行顺序和进入队列顺序相同
非公平:允许插队,如果一个线程请求非公平锁是正好锁为可用,则立马执行
大多数情况下非公平锁性能比公平锁好,因为线程从挂起到真正运行之间存在严重延时,公平锁每次都会经过此过程,需要额外消耗,但非公平锁可以执行抢占执行,省了此耗时
sync 是非公平的,看线程优先级和随机性,谁抢到算谁的
如何选择 rl 和 sync
在 JDK 1.5 之前 rl 性能更好,但是经过优化之后 sync 性能就追平了,而 sync 使用更简单,所有首选 sync
优化
将锁的粒度变小:同步代码块中的语句越少越好
将锁的粒度变大:争用特频繁的时候,例如如果在数据库表的每个字段都加锁,就会卡死
作为 monitor 的对象一定要声明为 final,防止重新赋值,否者线程竞争的锁就会发生改变,造成意想不到的结果
非阻塞同步(无锁优化)
CAS(Compare And set)
无锁优化:Unsafe 类
Unsafe 是单例
compareAndSwapXX
直接操作内存
cas(V, Expetcted, newValue)
V: 内存中的最新值
Expetcted:期望 V 的值
newValue:如果 V = Expected 则赋值,否则说明期间有线程更改了值,则将 E 赋值给 V 自旋再试。
CPU 原语支持,指令是原子性的
ABA 问题
基础数据没问题无所谓
但应用类型不行:你和前女友和你复合,但其实他已经经历了 N 个男人
解决办法:加 version 每操作一次 ++
AtomicXX 方法
AtomicInteger
AtomicLong
ThreadPoolExecutor
线程池本质是对线程和任务的管理,内部依靠生产者消费者模式来实现任务和线程的解耦
降低资源消耗:通过池化复用降低线程创建和销毁的损耗
提高响应速度:任务达到的时候,可复用线程无需等待创建
提高可管理性:线程是稀缺资源,如果无限创建会消耗系统资源,还会导致资源调度失衡。使用线程池可以集中管理、调度、调优
参数
corePoolSize 为线程池的基本大小。
maximumPoolSize 为线程池最大线程大小。
keepAliveTime 和 unit 则是非核心线程空闲后的存活时间
workQueue 用于存放任务的阻塞队列。
拒绝模式 handler :当队列和最大线程池都满了之后的饱和策略。
运行状态
线程池的状态并不是用户显式设置的,而是由线程池内部维护的
两个变量
runState :运行状态
workerCount:线程数量
这两个变量由一个 AtomicInteger 保存,高3位为 runState、低29位为 workerCount
用一个存储的好处:避免二者不一致,不用加锁保证一致性
RUNNING:能接受新的任务,并且能处理阻塞队列中的任务
SHUTDONW:关闭状态,不再接收新的任务,但却可以继续处理阻塞队列中保存的任务
STOP:不能接受新任务,也不能处理队列中的任务,会中断正在处理的任务
TIDYING:所有的任务都终止了,workerCount == 0
TERMINATED:在执行 terminated() 后进入该状态
任务管理
任务调度
检测线程池状态是不是 RUNNING,不是直接拒绝。线程池要保证在 RUNNING 的状态下执行任务
如果 wokerCount < corePoolSize 则创建并启动一个线程来执行新提交的任务
如果 wokerCount <= corePoolSize & 阻塞队列未满,则将任务放到队列中
如果 wokerCount >= corePoolSize && wokerCount < maximumPoolSize 且阻塞队列已满,这创建新的线程执行
如果 wokerCount >= maximumPoolSize && 阻塞队列慢了,这执行拒绝策略,默认的策略是直接抛弃
任务缓冲
线程池以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从队列中获取任务执行
阻塞队列
如果队列为空消费者将阻塞等待不为空
如果队列满了生产者将被阻塞等待队列有位置可用
获取任务
拒绝任务
此部分是线程池的保护部分,如果阻塞队列已满,并且线程池到点 maximumPoolSize 时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池
线程管理:worker
原理
worker 是 ThreadPoolExecutor 的内部类。实现了 AbstractQueuedSynchronizer (AQS)
没有使用可重入锁 ReentrantLock 而使用了 AQS 的目的是为了让 Worker 不可重入,仅使用 AQS 的state 记录线程状态
任务执行的时候会调用 lock 方法,一旦 lock 获取了独占锁,则表示线程在执行任务 此时 state = 1
如果 sate == 0 则表示没有执行任务
当用户调用线程池 shutdown 或 tryTerminate 时会触发 interruptIdleWorkers 方法尝试获取 work 的锁,如果获取成功这表示没有任务执行就调用 woker 内部持有线程的 interrupt 方法中断它
执行的是 woker#tryLock实际上是 CAS state 0 为 1 看是否成功
创建
调用 addWork
参数 firstTask:参数用于指定新增的线程执行的第一个任务
参数 core:
true 表示在新增线程时会判断当前活动线程数是否少于 corePoolSize
false 表示新增线程前需要判断当前活动线程数是否少于 maximumPoolSize
运行
在 Worker 类中的 run 方法调用了 runWorker 方法来执行任务
while 循环不断地通过 getTask() 方法获取任务。
getTask() 方法从阻塞队列中取任务。
如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
执行任务。
如果 getTask 结果为 null 则跳出循环,执行 processWorkerExit() 方法,销毁线程。
回收
依靠 JVM 回收的
在创建的时候会将 worker 的引用存储到 HasMap 中防止 JVM 回收
回收的时候从 HashMap 中移除,然后调用 tryTerminated 中断 worker 内部的线程
中断
shutdown: 等待任务执行完毕后退出
shutdownNow:立即退出
注意中断后线程将进入 terminated 状态,如果再次 start 会报错的需要重新 new 一个
也会触发 InterruptedException
常用实现
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
固定大小的线程池:核心线程数和最大数一致
LinkedBlockingQueue 的最大容量是 Integer.MAX_VALUE 太大了,相当于是无界的,任务太对会撑爆内存
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
核心线程数是 0
SynchronousQueue 不存放任何数据
基本上会为每个任务都创建线程,开销很大,所以适合密集型任务
线程会等待 60 秒所以叫 cached
newSingleThreadExecutor
core 和 max 都是 1
keepAliveTime = 0 便是永久存活
volatile
保证线程可见性
线程的内存模式会将变量拷贝到自己的工作区,但期间变量的值改变了对此线程是透明无感知的
保证缓存一致性,依据硬件完成的,对变量值更新有两个感知
总线嗅探,当被标记的变量改变时会通知所有线程此值失效,重新获取
禁止指令重排序
Double Check Lock 单例
防止在对象未初始化就赋给了变量(超高并发)使用的时候回有问题
对象初始化过程
申请堆内存
初始化对象
复制给变量
不能保证变量操作的原子性
开发习惯
给线程起个有意义的名字,这样好找 bug
多用线程池,不要手动创建线程,线程池对线程的管理更友好避免了重复创建线程造成的高开销
使用 BlockingQueue 实现生产者消费者问题
多用并发集合少用同步集合:使用 ConcurrentHashMap 而不是 HashTable
使用本地变量和不可变变量保证线程安全
内存模型
Java 内存模型视图屏蔽各种硬件和操作系统的内存差异,以实现 java 程序在各种平台下达到一致的内存方位效果
将主内存的数据拷贝到线程独立的工作内存中
这就是造成数据不同步引发并发问题的原因之一
守护线程
daemon 任何一个守护线程都是整个JVM中所有非守护线程的保姆
最有名的守护线程就是 GC
如果 JVM 中都是守护线程即所有非守护线程退出了,守护线程也就退出 JVM 熄火
可以通过 Thread.setDaemon(true) 开启,但是此过程必须在 start 之前否则抛出IllegalThreadStateException 异常。也就是说不能把正在运行的线程设置为守护线程
在守护线程中产生的新线程也是守护线程
BlockQueue
方法
take
从队头获取元素
如果队列为空将阻塞,直到有内容
put
将元素放置到队尾
如果队列为满 put() 将阻塞,知道队列有空闲位置
分类
FIFO 队列
LinkedBlockingQueue
ArrayBlockingQueue 固定长度
优先级队列
PriorityBlockingQueue
AQS 原理
AbstractQueuedSynchronizer:抽象队列同步器
使用链表实现的 FIFO 队列,当然可以设置为非公平的,将后来的插入头
关键变量
state
使用 volatile int state 来类型表示加锁的状态和加锁次数,为 0 表示没有锁,大于 0 表示有锁
加锁线程ID
记录获取锁成功的线程 ID
过程
线程1 使用 CAS 比较替换 state 如果内存数据是期望的 0 说明无锁这设置为 1 表示获锁成功没,然后用 CAS 将线程 ID 赋值给加锁线程 ID
线程2过来使用同样的方法获取锁,但是发现不是 1,获取失败,者进入 queue 开始排队
线程 1 每重入一次 sate++,没退出一下就 state--, 但 state = 0 ,就自动退出释放锁,将当前线程 ID 设置为 null
唤醒位于 queue 的线程2开始尝试加锁,这要看是不是公平锁
并发问题的原因
Java 内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。
工作内存保存了主存数据中的副本,线程对数据的操作都是在自己的工作内存中完成的,不会直接操作主内存
线程修改后的数据还保持在工作内存中,不会马上同步到主存中
主存的数据并不是最终结果,所以线程访问到的内容将会存在差异,造成结果有误