java并发编程
2023-08-22 17:28:08 0 举报
登录查看完整内容
java并发编程,学习思路,核心总结
作者其他创作
大纲/内容
概述
实现Callable接口
实现run()方法{业务逻辑}
实现Runable接口
其本身已经实现Runable
继承Thread类
线程的创建方式
为线程分配内存并初始化期成员变量的值
NEW 新建
JVM完成了方法的调用栈和程序计数器的创建
等待该线程的调度与运行
就绪状态
线程竞争到CPU资源
运行中状态
start方法后转为可运行状态
Runnable 可运行
运行中的线程主动或被动放弃CPU的执行权并暂停执行
等待阻塞
线程尝试获取正在被其他线程占用的对象同步锁
JVM会将该线程放入锁池中此时转为阻塞状态
同步阻塞
其他阻塞
阻塞状态分为三种
Blocked 阻塞
调用join会将等待的线程退出
Waiting 等待状态
Timed_Waiting 超时等待状态
Terminated 终止
JVM的状态标识
new新建
start启动
获取到CPU资源后进入运行中状态
可运行状态
等待状态
超时等待状态
流程
调用此方法 会释放锁
wait 线程等待
导致线程进入超时等待状态
不会释放锁
sleep 睡眠
yield
线程本身并不是因为更改中断标识而改变状态
抛出异常前会提前清除中断标识
注意点
具体变化由接收到中断标识之后程序具体处理结果而改变的
interrupt
主要是等待其他线程终止
等待另一个线程的执行结束
其结束后会将阻塞状态变为就绪状态
大多是用法是 主线程 等待所有子线程 结束后 继续执行
join
notifyAll 会唤醒在监视器上所有的线程
notify
setDaemon
sleep 不会对象释放锁
wait和sleep的区别
start和run的区别
线程的基本方法
线程的生命周期
原理
线程复用
创建并管理线程
线程池任务管理器
线程池中执行具体任务的线程
工作线程
定义工作线程的调度和策略
任务接口
存放待处理的任务
新任务不断加入该队列
执行完的任务从该队列移除
任务队列
线程池核心组件
只有execute()方法
Executor接口
继承 Executor
运行 创建后运行
增加生命周期的管理
ExecutorService接口
任务调度的线程池实现
可以在给定的延迟后运行命令或者定期执行命令
ScheduledExecutorService接口
ThreadPoolExecutor接口
java线程池通过Executor框架实现的
线程核心类
corePoolSize 线程池核心线程数
maximumPoolSize 线程池最大数
keepAliveTime 空闲线程存活时间
unit 时间单位
workQueue 线程池所使用的缓冲队列
threadFactory 线程池创建线程使用的工厂
handler 线程池对拒绝任务的处理策略
核心参数
ThreadPoolExecutor
能接受新提交的任务
并且也能处理阻塞队列中的任务
RUNNING 运行中
不再接受新提交的任务
但却可以继续处理阻塞队列中已保存的任务
调用 shutdown()方法
SHUTDOWN 关闭状态
不能接受新任务
也不处理队列中的任务
会中断正在处理任务的线程
调用 shutdownNow()
STOP 停止状态
如果所有的任务都已终止了
workerCount (有效线程数) 为0
线程池进入该状态后会调用 terminated()
进入TERMINATED 状态
TIDYING 整理中
在terminated() 方法执行完后进入该状态
默认terminated()方法中什么也没有做
TERMINATED 结束
线程池的生命周期
用于执行的线程队列
管理线程池的线程资源
创建
小于
该任务放入阻塞队列中
大于等于
正在执行的线程数与用户定义的核心线程数对比
创建非核心线程数立刻执行任务
大于或已满
线程空闲时间与配置对比
调用execute 添加一个任务线程时
原因
DiscardPolcy 丢弃当前任务不做任何处理
RejectedExecutionHandler 接口
捕获异常来实现自定义拒绝策略
JDK 内置的拒绝策略
线程拒绝策略
线程池的工作流程
短时间内创建的线程可复用
无任务时不占用系统资源
newCacheTreadPool 可缓存的线程池
newFIxedTreadPool 固定大小的线程池
可设置在指定延迟的时间后执行或者定期执行某个线程任务
newScheduledThreadPool 可做任务调度的线程池
有且只有一个线程
newSingleThreadExecutor 单个线程的线程池
一个任务拆成N个小任务
小任务分发多个线程执行
每个线程有自己的阻塞队列
当线程的阻塞队列没有任务了
就去别的线程队列获取任务执行
newWorkStealingPool 使用ForkJoinPool 实现的线程池 任务窃取线程池
五中常用线程池
线程池
主要用于保障多线程在并发的情况下数据一致性
通常需要使用对象或者调用方法前加锁
描述
但是在更新时会判断期间有没别人更新过数据
概念
比较版本号是否一致
具体流程
写操作之前比较当前值和传入值是否一致
CAS(Compare And Swap 比较和交换)
java乐观锁场景
乐观锁
在每次读数据之前都会认为别人要修改数据
所以每次读写之前都会加锁
先用CAS乐观锁去获取锁
获取不到就用悲观锁
AQS (abstract Queued Synchronized ) 抽象同步队列
java悲观锁场景
悲观锁
乐观和悲观的角度
指在分配锁之前检查是否有线程在排队等待获取锁
有则将锁分配至排队时间最长的线程
公平锁 FairLock
旨在分配锁时不考虑线程排队情况
synchronized是非公平锁
ReentranLock默认方法是采用公平锁
非公平锁NoFairLock
获取资源的公平性
ReentranLockReadWriteLock中的读锁为共享锁的实现
共享锁
独占锁
是否共享资源
偏向锁的实现过程包括:偏向锁的获取、偏向锁的撤销、偏向锁的重偏向。
偏向锁的获取:线程在进入同步块时,会先检查对象头中的标记位是否为偏向锁,并且线程ID是否与记录的ID一致,如果一致,直接进入同步状态。
偏向锁的撤销:当其他线程尝试获取偏向锁时,会撤销偏向锁,升级为轻量级锁。
偏向锁的重偏向:撤销偏向锁后,如果没有竞争再次进入同步块的线程,会将偏向锁重新偏向到该线程。
偏向锁
当一个线程尝试获取锁时,虚拟机会将对象头中的标记位设为“轻量级锁”,并将线程的Thread ID记录在对象头中。
如果其他线程也尝试获取同一个对象的锁,它们会进入自旋状态,不断尝试CAS操作来获取锁。
如果自旋失败,表示存在竞争,锁会膨胀为重量级锁。
轻量级锁的基本原理是使用CAS操作来避免线程进入阻塞状态。
轻量级锁的优点是避免了线程阻塞和唤醒的开销,减少了线程切换的次数,提高了程序的并发性能。
轻量级锁的缺点是自旋操作会消耗CPU资源,如果自旋时间过长,会导致性能下降。
轻量级锁是Java虚拟机为了提高多线程程序性能而引入的一种锁优化技术。
Mark Word中的标记位用来表示锁的状态,包括无锁状态、轻量级锁状态和重量级锁状态。
轻量级锁状态下,Mark Word中的指针指向线程的栈帧中的锁记录。
锁记录中包含了指向对象的指针和锁的状态。
轻量级锁状态下,线程通过CAS操作将对象头中的Mark Word替换为指向自己锁记录的指针。
如果CAS操作成功,表示获取锁成功;否则,表示存在竞争,线程会进入自旋状态。
自旋状态下,线程会不断尝试CAS操作来获取锁。
轻量级锁是通过对象头中的Mark Word来实现的。
轻量级锁的实现细节
轻量级锁适用于线程交替执行同步块的场景,且同步时间较短。
轻量级锁不适用于同步时间较长的场景,因为自旋操作会消耗CPU资源。
轻量级锁在多线程竞争激烈的情况下,容易膨胀为重量级锁,降低性能。
使用轻量级锁时,需要注意锁的粒度,尽量减小锁的范围,避免不必要的竞争。
轻量级锁的适用场景和注意事项
java的轻量级锁
轻量级锁
- 它是一种独占锁,即一次只能有一个线程持有该锁
- 当一个线程获取到重量级锁后,其他线程需要等待该锁释放才能继续执行
-重量级锁适用于保护竞争激烈的共享资源,但在性能上相对较差
重量级锁是Java中用于实现同步的一种锁机制
- Java中的重量级锁是通过操作系统的互斥量(Mutex)来实现的
-互斥量是一种特殊的变量,用于保护共享资源,确保同一时间只有一个线程可以访问该资源
- 当一个线程获取到互斥量后,其他线程需要等待该互斥量释放才能继续执行
重量级锁的实现依赖于操作系统的底层支持
独占性:一次只能有一个线程持有该锁。
阻塞性:当一个线程持有锁时,其他线程需要等待。
操作系统依赖性:重量级锁的实现依赖于操作系统的互斥量机制。
性能较差:由于需要操作系统的支持,重量级锁的性能较差。
重量级锁的特点如下:
- 竞争激烈的共享资源:当多个线程竞争同一个共享资源时,重量级锁可以确保只有一个线程能够访问该资源,避免数据错乱。
- 长时间持有锁:重量级锁适用于需要长时间持有锁的场景,因为它的性能较差,不适合频繁获取和释放锁的场景。
重量级锁适用于以下场景:
重量级锁
锁状态 从无锁->偏向锁->轻量级锁->重量级锁
随着锁的竞争而提升锁级别
综上所述
锁状态
读读不互斥
读锁
读写互斥
写写互斥
写锁
读写锁
锁分类
自旋锁是一种基于忙等待的锁,线程在获取锁时会不断地循环检查锁的状态,直到获取到锁为止。
1.概述
自旋锁的实现原理是通过CAS(Compare and Swap)操作来实现的,CAS操作是一种原子操作,用于判断内存中的值是否等于预期值,如果相等则将新值写入内存。
2.实现原理
a. 自旋锁避免了线程上下文切换的开销,适用于锁的持有时间很短的场景。
b. 自旋锁不会使线程进入阻塞状态,减少了线程切换的开销。
3.优点
a. 自旋锁需要不断地进行循环判断,如果锁的持有时间较长,会导致CPU资源的浪费。
b. 自旋锁不适用于只有一个CPU的场景,因为在单CPU上,自旋锁不会释放CPU资源。
4.缺点
a. 自旋锁适用于锁的竞争激烈但持有时间较短的情况。
b. 自旋锁适用于多核CPU的场景,可以充分利用多核CPU的并行性。
5.应用场景
Java中的自旋锁相关类有:AtomicInteger、AtomicReference、AtomicBoolean等。
6.相关类
1.5版本 固定时间
由上一次在同一个锁自旋时间及锁拥有者状态来决定的
可基本认为一个线程的上线文切换时间就是一个自旋时间
1.6版本 适应性自旋锁
自旋锁阈值
自旋锁
java中的每个对象都有个monitor对象
加锁就是在竞争monitor对象
对代码块加锁就是通过在前后分别加上 monitorEnter和monitorExit指令实现的
对方法是否加锁是通过一个标记判断的
作用于成员变量和非静态方法时
作用于静态方法时
锁住的是在代码块中配置的对象
作用于一个代码块时
作用范围
所有请求锁的线程都被放在锁竞争队列中
ContentionList 竞争锁队列
在Contention中有资格成为候选者来竞争锁资源的线程被移动到了EntryList
EntryList 竞争候选队列
调用wait方法后被阻塞的线程放在waitSet
WaitSet等待集合
onDeck 竞争候选者
竞争到锁资源的线程被称为Owner线程
Owner
!Owner
原理\t
为防止锁竞争时ContentionList尾部的元素被大量的并发线程CAS访问影响性能
Onwer线程在释放锁资源时将ContentList中的部分线程移动到EntryList队列中
并指定EntryList中某个线程(一般是最先进入的线程)为OnDeck线程
Java把该行为称为\"竞争切换\
非公平的锁
重量级操作
1.6版本默认开启了偏向锁和轻量级锁
可以通过 -XX:UseBiasedLocking 禁用偏向锁
1.6版本后
Java中的关键字,用于实现线程的同步
保证在同一时刻只有一个线程可以执行被synchronized修饰的代码块或方法
确保了多个线程之间的数据同步,避免了并发访问的问题
synchronized可以修饰代码块或方法
修饰代码块时,需要指定一个对象作为锁,同一时刻只有一个线程可以获取到该对象的锁
修饰方法时,锁对象默认为当前对象实例,同一时刻只有一个线程可以执行该方法
使用方式
使用了Java中的内置锁机制(monitor)
每个对象都有一个monitor,用于实现对对象的同步访问
当一个线程获取到对象的锁时,其他线程无法访问该对象的synchronized代码块或方法,只能等待
实现原理
独占性:同一时刻只能有一个线程获取到锁
可重入性:线程可以重复获取已经持有的锁
互斥性:获取到锁的线程会进入临界区,其他线程无法同时进入
锁的特性
类锁:作用于整个类,只有一个线程可以获取到该锁
对象锁:作用于对象实例,同一时刻只有一个线程可以获取到该对象的锁
方法锁:作用于方法,同一时刻只有一个线程可以执行该方法
锁的粒度
避免过多使用synchronized,会降低程序的性能
尽量使用同步块而不是同步方法,可以减小锁的粒度,提高并发性能
避免在同步块中进行耗时操作,会导致其他线程长时间等待
注意事项
java中的synchronized
synchronized
是一个可重入锁
通过自定义队列同步器 AQS 来实现锁的获取与释放
该锁同一时刻只能被一个线程获取
获取锁的其他线程只能在同步队列中等待
支持一个线程对同一个资源执行多次加锁操作
可重入锁
需要加锁时通过lock方法加锁
需要解锁时通过unlock方法释放锁
释放次数多时会抛出异常
加锁次数多时会一直占用锁资源
获取锁的次数要与释放锁的次数相同
显式操作
用法
响应中断
通过trylock来查询是否有可用的锁
可轮询锁
当前线程获取到可用锁并返回了true
定时锁
避免死锁
获取到可用锁资源则加锁
lock给对象加锁
无可用资源返回false
trylock()试图给对象加锁
只能由持有者释放锁
unlock 解锁
线程未中断则获取该锁
lockInterruptibly()
Lock接口主要方法
都是用于控制多线程对共享对象的访问
都是可重入锁
都保证了可见性和互斥性
共同点
API级别的
可以定义为公平锁
是一个接口
可以知道是否获取到锁资源
可以定义读写锁提高多个线程的读操作效率
ReentranLock
隐式释放锁
JVM级别的
Java中的关键字
无法感知到锁是否获取到
不同点
synchronized与ReentranLock的对比
基于计数的信号量
在定义信号量对象时可以设定一个阈值
Semaphore对锁的申请和释放与ReentranLock类似
通过acquire方法和release来获取和释放许可信号资源
acquire也可中断取消许可信号的申请
释放许可需在finaly代码块中完成
也可创建计数为1的Semaphore
应用
Semaphone
AtomicBoolean 用于在多线程环境中对boolean类型的变量进行原子操作
AtomicInteger 用于在多线程环境中对int类型的变量进行原子操作
AtomicReference 用于在多线程环境中对引用类型的变量进行原子操作
性能通常是synchronized和ReentranLock的好几倍
AtomicInteger的原子操作类
AtomicInteger
也叫递归锁
旨在同一线程中外层函数获取到该锁之后
内层的递归函数可以继续获取该锁
JDK1.7及之前版本的ConcurrentHashMap在内部就是使用分段锁实现的
分段锁
同步锁和死锁
减少锁持有的时间指只有在线程安全要求的程序上加锁来尽量减少同步代码块对锁持有的时间
减少对锁的持有时间
将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来减少同一个锁的竞争
最经典的案例就是1.7版本之前的ConcurrentHashMap的分段锁
减少锁粒度
根据功能将锁分离成读锁和写锁
锁分离
锁粗化
这时我们需要检查并消除不必要的锁来提高系统性能
锁消除
如何进行锁优化
java中的锁
CPU利用时间片轮询来为每一个任务都服务一定时间
任务状态的保存及再加载过程叫上下文切换
上下文切换
一个进程多个线程
并创建它的进程共享同一地址空间(一段内存空间)和其他资源
一个运行中程序的实例
进程
线程切换时CPU寄存器和程序计数器所保存的当前线程信息
上下文
CPU内部容量较小但速度较快的内存区域(对于CPU外部相对较慢的是RAM主内存)
寄存器通过对常用值(通常运算的中间值)的快速访问来加快计算机程序运行的速度
寄存器
是一个专用的寄存器
用于表明指令序列中CPU执行的位置
程序计数器
内核(操作系统的核心)在CPU上对进程或者线程进行切换
上下文切换过程中的信息被保存在进程控制块(PCB)
PCB又被称作切换桢
简述
在PCB中检索下一个线程的上下文并将其在CPU的寄存器中恢复
跳转到程序计数器所指的位置(即转到线程被打断时的代码块)并恢复该线程
时间片轮转方式使多个任务在同一CPU上执行有了可能
硬件中断
导致上下文切换的原因
线程上下文切换流程
上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。
线程上下文切换带来的问题
无锁并发编程,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁
使用最少线程
协程,单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
优化方案
高并发,低耗时的情况,建议少线程
低并发,高耗时的情况:建议多线程
高并发高耗时,要分析任务类型、增加排队、加大线程数
合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销
线程的上下文切换
Java中的阻塞队列
允许一个或多个线程一起等待其他线程操作执行完毕后在执行相关操作
例如主线程等待其他子线程执行完毕后在执行主线程相关操作
主线程定义CountDownLatch
并将线程计数器初始值设置为子线程个数
此时在countDownLatch等待的主线程将被唤醒并继续执行
使用过程
CountDownLatch
CyclicBarrier(循环屏障)
信号量
Semaphore
是不可用重用的
CountDownLatch主要用于主线程等待其他子线程任务执行完毕后在执行主线程任务
可以重用的
可见性
有序性
使用volatile修饰变量有两大特性
因此volatile是一种比synchronized关键字更轻量级的同步机制
主要适用于一个变量被多个线程均可对这个变量执行赋值或者读取操作
每个线程首先需要将数据从内存复制到CPU缓存中
这样每个线程都针对这个变量做了不同操作后就可能存在数据不一致的情况
普通线程
有效的解决多线程数据同步的问题
将变量声明为volatile
与普通变量对比
但不能保障像i++这种操作的原子性
volatile在某种场景下可以代替synchronized但是不能完全替代synchronized
该变量没有被包含在具有其他变量的不等式
也就是说在不同的volatile变量之间不能相互依赖
只有状态真正独立于程序内的其他内容才能使用volatile
必须满足以下两种条件才能保证并发环境下的线程安全
需要说明的是
标记变量:在多线程环境下,使用volatile修饰标记变量可以实现线程间的通信
双重检查锁定(Double-Checked Locking):使用volatile修饰的变量可以保证在多线程环境下,单例模式的实例只被创建一次
状态标志位:使用volatile修饰的状态标志位可以实现线程的停止
使用场景
volatile
多线程的通信主要是通过共享内存实现的
原子性
程序执行的顺序按照代码的先后顺序进行执行
可见性
共享内存的主要三个关注点
多线程如何共享数据
java并发关键字
Fork/Join并发框架
每个线程都以抢占的方式获取CPU资源并快速执行
在执行完毕后立刻释放CPU资源
具体哪些线程抢占到CPU资源由操作系统控制
每个线程对CPU资源的申请地位都是相等的
从概率上讲每个线程都有概率获取到CPU执行时间片并发执行
抢占式调度适用于多线程并发的情况
这种机制下一个线程的堵塞不会导致整个进程性能下降
抢占式调度
线程对CPU执行的时间由线程自身控制
协同式调度
java会为每个线程按照优先级高低分配不同的CPU时间片
且优先级高的线程优先执行
优先级低的线程只是获取CPU时间片的优先级被降低
但不会永远分配不到CPU
在保障效率的前提下尽可能保障线程调度的公平性
Java线程调度的实现:抢占式
当前的线程进入阻塞状态
当前线程运行结束
线程让出CPU的情况
java中的线程调度
先来先服务调度算法
每次调度时都从队列中选择一个预估运行时间较短的作业
以提高CPU整体的利用率和系统运行效率
某些大型任务可能会出现长时间得不到调度的情况
短作业优先调度算法
优先调度算法
每次调度时都选择优先级最高的任务
非抢占式调度
变化规律如下
在保障效率的情况下尽可能提高了公平性
高响应比优先调度算法
定义任务时为每个任务都设置不同的优先权
高优先权优先调度算法
调度器收到该信号后中断该任务并将该任务放入就绪队列的队尾
然后从队头取出一个任务并为其分配CPU时间片去执行
时间片轮转算法
在时间片轮转调度算法基础上设置多个就绪队列
并为每个就绪队列都设置不同的优先权
多级人物反馈队列调度算法
基于时间片的轮转调度算法
进程调度算法
CAS(Compare And Swap) 比较并替换
V表示要更新的值
E表示预期值
N表示新值
V值=E值 才会将V值设为E值
最后CAS返回当前V的真实值
采用了乐观锁的思想
总是认为自己可以成功完成操作
CAS的特性
CAS自旋等待
CAS
每次执行数据的修改操作时都会带上一个版本号
部分乐观锁是通过版本号来解决ABA问题
ABA问题
AQS(Abstract Queued Synchronizer) 是一个抽象的队列同步器
通过维护一个共享的资源状态(volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程并发访问共享资源的同步框架
AQS为每个共享资源都设置一个共享资源锁
线程在访问共享资源时先获取共享资源锁
对状态的变更使用了 Unsafe
state状态
初始值为0无锁状态
获取锁成功后state状态+1
直到释放锁state-1
当状态为0时其他线程才能获取到锁
具体的java实现有ReentranLock
只有一个线程执行
独占式
state的初始值和分割子线程的个数一样
子任务每执行完成一次 都会state-1
所有执行完成后state为0
这时会unpark()主线程
具体java实现有 Semaphore和CountDownLatch
多个线程可以同时执行
共享式
读时共享式
写入时独占式
ReentranReadWriteLock
也可以同时实现 独占式和共享式
AQS共享资源的方式
不同自定义同步器争取的共享资源方式也不同
自定义同步器在实现只需要实现共享state的获取与释放方式即可
AQS只是定义了一个接口
AQS
流是对数据集操作的定义
将一个流转换成另一个流
其目的是建立一个流水线
map 映射新元素
filter 过滤
distinct 去重
limit 截取
skip(n) 跳过
sorted 排序
常见中间操作函数
中间操作(如filter或map)
在执行终端操作时会触发函数计算
产生一个最终的结果并返回
支持类型的操作
流的出现为高效的聚合操作和大批量数据操作提供了方便
并行流能够充分利用处理器多核的优势并提高数据的处理效率
通过fork/join并行方式来拆分任务和加速处理过程
流可分为串行流和并行流
以处理数据是否在多个线程上执行来区分
区分
串行流上的数据计算过程中是在一个线程上以串行的方式逐个被处理的
串行流
这样数据的处理任务便可以以多线程的方式在多核上并行处理
并行流
并行流(parallelStream)和串行流(Stream)的原理
Java8中的流
并发
0 条评论
回复 删除
下一页