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