并发多线程
2023-12-04 17:45:16 0 举报
AI智能生成
java
作者其他创作
大纲/内容
线程安全
运行结果错误(多线程操作一个变量)
发布和初始化导致线程安全问题,13;对象还未初始化成功就被其他线程调用
活跃性问题
死锁
死锁是指两个线程之间相互等待对方资源,13;但同时又互不相让,都想自己先执行
活锁
正在运行的线程并没有阻塞,它始终在13;运行中,却一直得不到结果
饥饿
饥饿是指线程需要某些资源时始终得不到,13;尤其是CPU 资源,就会导致线程一直不能13;运行而产生的问题
线程安全可能会出现场景,13;①共享变量或者共享资源。13;②依赖时序的操作。13;③不同数据之间存在绑定关系。13;④没有声明自己是线程安全的工具类
基础知识
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程或在各个线程之间切换工作时,负担要比进程小得多,线程也被称为轻量级进程。
线程生命周期:13;继承Thread方法,13;实现Runnable接口
停止线程:使用interrupt通知线程停止,stop等强行停止线程方法被废弃。
Daemon线程(守护线程)是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
sleep与wait 区别
JAVA锁
乐观/悲观锁
乐观锁(一般实现CAS)是一种乐观思想,即认为读多写少,遇到并发写的可能性低。采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
自旋锁/非自旋锁
自旋锁,它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。
非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。
公平/非公平锁
公平锁(按顺序):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
非公平锁(不按顺序):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
分支主题
1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列 ,13;2. Java中的synchronized是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
共享/独占锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种 乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁
可/不可重入锁
可重入锁/递归锁:指的是同一个线程外层函数获得锁之后,内层仍能获取该锁,在同一个线程在外层方法获取锁时,在进入内层方法或会自动获取该锁。(ReentrantLock /synchronized)
不可重入锁/自旋锁: 所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
并发编程
volatile
JMM
线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量
重排序
为了程序的性能,处理器、编译器都会对程序进行重排序处理
条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
问题
重排序在多线程环境下可能会导致数据不安全
happens-before
JMM中最核心的理论,保证内存可见性
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
理论
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
两个操作之间存在happens-before关系,并不意味着一定要按照happens -before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
as-if-serial
所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变
MESI13;(缓存一致性协议)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,就会从内存重新读取。
需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。(总线风暴)
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
AutomicInteger
保证原子性
底层unsafe类,CAS原理
atomicInteger.compareAndSet(expectedValue,newValue)
特性
可见性
嗅探机制,强制失效
处理器嗅探总线
有序性
禁止指令重排
lock前缀指令,内存屏障
源代码->编译器优化重排序->指令级并行重排序13;->内存系统的重排序
happen-before
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
as-if-serial
编译器和处理器不会对存在数据依赖关系的操作做重排序
原子性
volatile对单个基本数据类型读/写具有原子性,但是复合操作除外,例如i++
synchronized
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
锁对象
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
内存对象
Java对象头
synchronized的锁就是保存在Java对象头中的
包括两部分数据
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳
Klass Pointer(类型指针)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定13;这个对象是哪个类的实例。
实例数据
对齐填充
monitor
Owner:线程号13;count:重入次数
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
锁优化
自旋锁
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
自旋字数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失
缺点
自旋次数无法确定
适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据
变量逃逸
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。(单向,不降级)
JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定, 使用偏向锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏斜过的对象, JVM 就撤销偏向锁, 切换到轻量级锁实现。
轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功, 就使用普通的轻量级锁;否则,进一步升级为重量级锁
底层原理:属于 JVM 层面,在对象头中设置标记实现。每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象。代码块加锁是在前后分别加上monitorenter 和monitorexit 指令来实现的,底层基于互斥锁。
区别
lock
ReentrantLock
ReentrantLock 以及所有的基于 Lock 接口的 实现类,都是通过用一个 volitile 修饰的 int 型变量,并保证每个线程都能拥有对该 int 的可见性和原子修改,其本质是基于 AQS 框架。
ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。
ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。
ReentrantReadWriteLock13;(可以设置为公平或者非公平)
ReadLock
WriteLock
要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)。
CAS
Compare And Swap,整个JUC体系最核心、最基础理论
内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干
native中存在四个参数
缺陷
循环时间太长
只能保证一个共享变量原子操作
ABA问题
解决方案
版本号
AtomicStampedReference
AQS
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件
加解锁
锁状态
state 为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
线程的阻塞和解除阻塞
采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程
阻塞队列
实现原理
结构
volatile int state(代表共享资源)
代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁;可重入+1
volatile能够保证多线程下的可见性
操作都是通过CAS来保证其并发修改的安全性
volatile Node head(头结点,未入队列)
当前持有锁的线程
volatile Node tail
阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
Thread exclusiveOwnerThread
代表当前持有独占锁的线程
等待队列
分支主题
等待队列中每个线程被包装成一个 Node 实例,数据结构是链表
Node包括thread + waitStatus + pre + next 四个属性
阻塞队列
基本原理
1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列
2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
ArrayBlockingQueue
一个由数组实现的FIFO有界阻塞队列
有界且固定,在构造函数时确认大小,确认后不支持改变
在多线程环境下不保证“公平性”
实现
ReentrantLock
Condition
LinkedBlockingQueue
基于链接,无界的FIFO阻塞队列
PriorityBlockingQueue
支持优先级的无界阻塞队列
默认升序排序,可指定Comparator排序
二叉堆
分类
最大堆
父节点的键值总是大于或等于任何一个子节点的键值
最小堆
父节点的键值总是小于或等于任何一个子节点的键值
添加操作则是不断“上冒”,而删除操作则是不断“下掉”
实现
ReentrantLock + Condition
二叉堆
DelayQueue
支持延时获取元素的无界阻塞队列
应用
缓存:清掉缓存中超时的缓存数据
任务超时处理
实现
ReentrantLock + Condition
根据Delay时间排序的优先级队列:PriorityQueue
Delayed接口
用来标记那些应该在给定延迟时间之后执行的对象
该接口要求实现它的实现类必须定义一个compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
SynchronousQueue
一个没有容量的阻塞队列
应用
交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务
难搞懂,与Exchanger 有一拼
LinkedTransferQueue
链表组成的的无界阻塞队列
相当于ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集
预占模式
有就直接拿走,没有就占着这个位置直到拿到或者超时或者中断
LinkedBlockingDeque
由链表组成的双向阻塞队列
容量可选,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
运用
“工作窃取”模式
其他
ThreadLocal
ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制
四个方法
get():返回此线程局部变量的当前线程副本中的值
initialValue():返回此线程局部变量的当前线程的“初始值”
remove():移除此线程局部变量当前线程的值
set(T value):将此线程局部变量的当前线程副本中的值设置为指定值
ThreadLocalMap
实现线程隔离机制的关键
每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本
无链表,解决hash冲突的办法是线性探测
注意点
ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值的key
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中
脏数据/内存泄漏问题
线程复用产生脏数据
ThreadLocalMap
key 弱引用 value 强引用,无法回收
解决显示调用remove()
Fork/Join
一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
核心思想
“分治”
fork分解任务,join收集数据
工作窃取
某个线程从其他队列里窃取任务来执行
执行块的线程帮助执行慢的线程执行任务,提升整个任务效率
队列要采用双向队列
核心类
ForkJoinPool
执行任务的线程池
ForkJoinTask
表示任务,用于ForkJoinPool的任务抽象
ForkJoinWorkerThread
执行任务的工作线程
线程池
基本原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行。
主要特点:1.线程复用;2.控制最大并发数量;3.管理线程
1.降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。13;2.提高相应速度,当任务到达时,任务可以不需要的等到线程创建就能立即执行。13;3.提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅仅会消耗系统资源,还会降低体统的稳定性,使用线程可以进行统一分配,调优和监控。
构造方法
Executors
静态工厂类,提供了Executor、ExecutorService、ScheduledExecutorService、 ThreadFactory 、Callable 等类的静态工厂方法
Executors.newSingleThreadExecutor()
只有一个线程的线程池,因此所有提交的任务是顺序执行
Executors.newCachedThreadPool()
线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒未执行,将被终止并从池中删除
Executors.newFixedThreadPool()
拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待
Executors.newScheduledThreadPool()
用来调度即将执行的任务的线程池
Executors.newWorkStealingPool()
能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中
核心参数
corePoolSize
核心线程数
maximumPoolSize
workQueue
任务队列,被提交但尚未被执行的任务
keepAliveTime
当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
RejectedExecutionHandler
拒绝策略
抛异常
丢弃
重试
丢弃最早
运行过程
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
2.当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行
3.当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务
4.当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler处理
5.当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true) 时,线程池中 corePoolSize 线程空闲时间达到 keepAliveTime 也将关闭
配置线程池
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
0 条评论
下一页