java并发编程
2022-05-18 22:55:49 0 举报
AI智能生成
java并发知识相关总结
作者其他创作
大纲/内容
并发集合
queue
TransferQueue
deque
BlockingDeque
LinkedBlockingDeque
copyonwriter
CopyOnWriteArrayList
CopyOnWriteArraySet
concurrent
ConcurrentLinkedQueue
ConcurrentHashMap
ConcurrentSkipListMap
ConcurrentSkipListSet
阻塞队列
BlockingQueue
ArrayBlockingQueue
数组形式存储
存和取采用同一把锁进行,基于condition进行处理
为什么put/take 存储和取出时都要唤醒对立的线程
LinkedBlockingQueue
链表形式存储,putIndex,takeIndex 单独存在,两把锁
put
存储前的容量小于capacity时存储后唤醒其他处于阻塞的存储线程,为什么需要这样设计;
因为移除元素时采用的是signal()并非signalAll();
因为移除元素时采用的是signal()并非signalAll();
take
PriorityBlockingQueue
优先级队列,基于小顶堆的形式存储(首先是一个完全二叉树,左右子树的节点都大于等于父节点),
假设父节点index=0,那么左子树index=2*i+1,右子树节点index=2*i+2 (数组存储,index从0开始)
假设父节点index=0,那么左子树index=2*i+1,右子树节点index=2*i+2 (数组存储,index从0开始)
SynchronousQueue
结构
公平
TransferQueue
基于head.tail的单向链表结构存储,第一次put时由于head.tail指向同一个节点,那么先设置tail的尾节点为新数据的指向(casNext)
,接着替换尾节点(advanceTail),为新插入的节点然后阻塞;
,接着替换尾节点(advanceTail),为新插入的节点然后阻塞;
take获取数据时,先获取head.tail数据,
先从head的next中获取数据(m),通过cas设置m的item值为null,接着替换头节点advanceHead,唤醒m节点对应的线程;
先从head的next中获取数据(m),通过cas设置m的item值为null,接着替换头节点advanceHead,唤醒m节点对应的线程;
结构
单向链表.先进先出
head.tail
非公平(默认)
TransferStack
结构
head,每次插入数据,都casHead数据
先进后出
原理
也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)
DelayQueue
延迟队列,基于delay方法 返回延迟时间与当前时间的差值
若大于0说明未到执行时间,底层采用优先级队列存储
若大于0说明未到执行时间,底层采用优先级队列存储
atomic
LongAdder
数组+cas
AtomicLong
cas 某一个变量在极高并发的情况下cpu空转问题严重
lock
ReentrantLock
独占锁
ReentrantReadWriteLock
加锁特征
读读不互斥
读写互斥
锁的 升降级
同一个线程先加写锁 再加读锁 允许 锁的降级
同一个线程先加读锁 再加写锁 不允许 锁的升级
state
高16位为读锁的加锁次数
低16位为写锁的加锁次数
读写锁的加锁次数最大为1<<<16 -1
线程唤醒
写锁唤醒读锁,只唤醒第一个节点的线程,然后由第一个节点唤醒第二个节点, 依次唤醒 唤醒风暴
StampedLock
特点
悲观读锁
基于时间戳
乐观读
独占写
使用实例
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
AbstractQueuedSynchronizer
node状态
cancel 1
默认状态 0
signal -1
condition -2
PROPAGATE -3
该状态是为了修复jdk1.6的bug,针对共享锁存在的问题
Condition
使用
await
释放当前线程所占的锁,添加到condition队列中(单向链表),节点状态为CONDITION,nextWaiter不为空,prev,next为空
从await中被中断唤醒后,将自己的waitstatus状态设置为0,并且加入到clh队列中,重新抢占锁,抢占成功后,抛出中断异常
signal
唤醒condition链表的第一个节点,并将其加入到clh队列中,waitstatus状态归0,前一个节点状态置为-1
原理
基于单链表存储处于阻塞的线程,唤醒后移入clh队列中抢占锁
线程池
Future
Callable
ThreadPoolExecutor
优点
便于资源管理和控制
降低创建线程和销毁线程的性能开销
提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
标准线程池
newScheduledThreadPool
可调度
newCachedThreadPool
任务耗时较短,任务量大
newSingleThreadExecutor
单个线程
newFixedThreadPool
普通任务
newWorkStealingPool
forkjoin
核心参数
coreSize
核心线程数
maximumPoolSize
最大线程数
keepAliveTime
最大线程与核心线程的差值线程存活时间
BlockingQueue<Runnable> workQueue
阻塞队列
ThreadFactory threadFactory
线程工厂
RejectedExecutionHandler (拒绝策略)
AbortPolicy
抛出异常
CallerRunsPolicy
在当前线程执行
DiscardOldestPolicy
丢弃下一个将要执行的任务
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
e.getQueue().poll();
e.execute(r);
}
DiscardPolicy
静默的丢弃,不抛出异常
线程池管理
线程池状态
RUNNING
接受任务
SHUTDOWN
不在接受任务 ,但队列中的任务仍然执行
STOP
不接收新任务,不执行 队列中的任务,中断正在执行中的任务
TIDYING
所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
TERMINATED
terminated()方法 执行完成
线程池数量
AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
COUNT_BITS = Integer.SIZE - 3
高3位代表线程池状态,低29位为线程数量
线程管理
Worker
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
extends AbstractQueuedSynchronizer
implements Runnable
独占锁
独占的目的是持有锁代表正在执行任务,未持有锁说明可中断
调用shutdown.setCoreSize(核心线程数减小) 会中断空闲线程
线程释放及keeplive作用
从阻塞队列去任务超时时间
interruptIdleWorkers
中断所有线程时,如果线程处于阻塞队列的await下,那么会被中断状态所唤醒,从而抢占锁成功,最终run方法执行结束,线程数量递减,回归
监控
prestartAllCoreThreads
预初始化核心线程数
getCompletedTaskCount
完成任务数
beforeExecute
任务执行前
afterExecute
任务执行后
其他
ThreadLocal
内存泄漏
由于threadLocalMap的key是一个弱引用(在下次垃圾回收时就会被回收);首先value为50M的大对象,thread为线程池核心线程(可复用,不会被回收),threadLocal已手动置为null;此时threadLocalMap的entry的ke由于是弱引用在下次垃圾回收时被回收,但是value还是Gc可达,导致无效内存长期占用内存空间不会被回收
原理
基于线程本地变量进行保存数据,实现线程隔离;数据保存在线程的threadLocalMap中;
key为ThreadLocal对象(若为thread对象,完全不需要map,但是保存数据只有1个);
key为ThreadLocal对象(若为thread对象,完全不需要map,但是保存数据只有1个);
hash冲突解决
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618…的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。
ThreadLocalMap中的set()
ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入
ThreadLocalMap中的set()
ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入
set hash定位逻辑
脏数据指: hash桶里的entry不为空,但是key为空
可覆盖的数据指: key相同,使用新值覆盖旧值
清理entry不为空但是key为空的数据
向前(循环)存在脏数据(slotToExpunge=3)向后(并非到len结束也可循环)存在可覆盖(i)的数据
清理范围 slotToExpunge-len
向前存在脏数据向后不存在可覆盖的数据
清理范围 slotToExpunge-len
向前不存在脏数据,向后存在可覆盖(i)的数据集
清理范围 i-len(之所以从i开始,因为获取可覆盖角标时前面的数据都以判定为不为空,无需重复清理)
向前不存在脏数据,向后不存在可覆盖的数据
清理范围 初始定位值-len
假设len=16,staleSlot=4
有脏的代表entry不为空但是key为null ,可覆盖代表两个key一样后者可覆盖前者
slotToExpunge 值为脏数据 i为可覆盖数据角标
有脏的代表entry不为空但是key为null ,可覆盖代表两个key一样后者可覆盖前者
slotToExpunge 值为脏数据 i为可覆盖数据角标
Fork-join
FutureTask
状态
New
只有状态为New时才允许取消
COMPLETING
任务执行完成 但是还未设置结果
NORMAL
任务正常执行完成,但还未唤醒阻塞线程
EXCEPTIONAL
任务执行出现异常,
CANCELLED
任务取消
INTERRUPTING
中断线程前
INTERRUPTED
中断任务结束
阻塞线程
单向链表WaitNode(后进先出)
runnable->callable
get
任务完成
任务取消,抛出Cancel异常
任务中断,抛出ExecutionException
当前阻塞的线程被中断,抛出InterruptedException
任务取消
状态为New时才允许取消, 并唤醒阻塞线程
中断执行任务线程
aba问题
AtomicReference
ThreadLocalRandom
协调组件
CyclicBarrier
原理
基于ReentrantLock实现+Condition 阻塞
特点
使一组线程之间互相等待,直到所有线程都到达终点才唤醒,可循环使用
最后一个线程到达终点 执行command
使用
await
阻塞
某一个中断唤醒所有的线程
await(long timeout, TimeUnit unit)
待超时阻塞
breakBarrier
唤醒所有处于等待的线程
CountDownLatch
读写锁一种
使一个或者多个线程等待,等其他线程将countDown释放为0时唤醒
await
sate<=0 则阻塞 等待唤醒
countDown
state=0 则唤醒clh节点的线程
Semaphore 信号量
特点
计数器限流
spring-cloud-hystrix 策略 默认为ThreadPool 信号量也支持
原理
acquire
获取多个令牌,获取不到则阻塞
state数量是否足够,足够 则获取成功 state-permit,否则阻塞
release
释放令牌
归还state state+permit并唤醒等待令牌的线程
特点
释放成功后就唤醒处于阻塞的线程,不用state等于0
countdownlatch需要state等于0才唤醒
chm
方法
putIfAbsent
当可以不存在时,才执行后续的值,可能返回null
computeIfAbsent
当可以不存在时,才执行后续的值,不可能返回null
merge
数据合并
特点
数据结构
数组+链表+红黑树
链表转换为红黑树条件
数组长度>64
链表长度大于8
size统计
baseCount (cas)+CountCell数组
CountCell
VOLITALE 标记的数值
sun.misc.Contended
填充缓存行
CELLSBUSY 作为cas标记位
并发协助扩容
扩容戳
高16位 基于当前数组长度计算出的高16位代表扩容戳,
低16位-1 代表扩容线程数量(-1为初始化中) +2
可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n,这个生成
戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同
戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同
ForwardingNode
迁移节点的标记位,标记为此处已迁移数据完毕
TRANSFERINDEX
转移数据的索引值
扩容结束标记
最后一个扩容线程使用cas进行sc-1, 然后sc-2 >> 16 发现等于初始扩容戳,即扩容结束,重新double check下
transferIndex
分段扩容已分配完毕
数据迁移
高低位迁移
原来索引位置取决于hash&length-1,当数组扩容时,(16->32),二进制值为(1 0000 10 0000) 原来长度位 1111 -> 1 1111,所以扩容时只需要看原来hash的第一位高位的值是否为1 11111 刚好又是原来数组长度;
高低位迁移时有位置不变的,也有位置变化的+原有数组长度,两者是否存在冲突(不同线程间);不会:
假设数组原长度为32 ,扩容后数组长度为64,线程1 迁移index为31-16 线程2迁移位置为15-0;
线程1的高低位只能为63-48,31-16 而线程2的高低位为47-32,15-0;他们之前不会冲突
假设数组原长度为32 ,扩容后数组长度为64,线程1 迁移index为31-16 线程2迁移位置为15-0;
线程1的高低位只能为63-48,31-16 而线程2的高低位为47-32,15-0;他们之前不会冲突
迁移完数组中某个位置的元素后 会在此处设置一个MOVE 代表此处已迁移完成,但是由于数组引用地址未变更,所以还是会在老数组上进行get.put 操作,需要等待迁移数据线程完成数据迁移,才能执行后续操作
最后一个扩容线程会double check下,sc-1 (sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
断是否需要扩容,也就是当更新后的键值对总数 baseCount >= 阈值 sizeCtl 时,进行
rehash,这里面会有两个逻辑。
1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
2. 如果当前没有在扩容,则直接触发扩容操作
rehash,这里面会有两个逻辑。
1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
2. 如果当前没有在扩容,则直接触发扩容操作
是否参与扩容判断
(sc >>> RESIZE_STAMP_SHIFT) != rs
当前容量与正在扩容的容量不一致;表示比较高 RESIZE_STAMP_BITS 位
生成戳和 rs 是否相等,相同
生成戳和 rs 是否相等,相同
由于原rs的高16位始终为0,只有低16位有值,所以rs<<16 位加2 得到sc(低16位变成高16位,),将sc>>>16(原有的高位又变成低位,由于原来的高16位时钟为0,那么此时数据还原);
sc == rs + 1
表示扩容结束
扩容结束的最后一个线程会将其减1
sc == rs + MAX_RESIZERS
达到可参与最大扩容线程数
transferIndex <= 0
表示所有的 transfer 任务都被领取完了,没有剩余的
hash 桶来给自己自己好这个线程来做 transfer
hash 桶来给自己自己好这个线程来做 transfer
(nt = nextTable) == null
表示扩容已经结束
rs=Integer.numberOfLeadingZeros(n) | (1 << (16 - 1))
此值计算出来始终保证该值左移16位时最高位为1 ,为负数
Integer.numberOfLeadingZeros(n) 由于n越大 最高位非0位0的个数始终在减少,
最大值为16对应的值为27 与(1<<15)计算后最大值 为32795 ,每次扩容就依次减少, 所以rs的高16位始终为0
最大值为16对应的值为27 与(1<<15)计算后最大值 为32795 ,每次扩容就依次减少, 所以rs的高16位始终为0
步长分配
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
stride = MIN_TRANSFER_STRIDE;
最小步长为16 ,cpu核数为1 则为当前数组长度,否则原数组长度/8 比上16
其他
hash值计算
(h ^ (h >>> 16)) & HASH_BITS
sizeCtl
-1 代表数组初始化
(rs << RESIZE_STAMP_SHIFT) + 2
>=0 说明未初始化容量或者阈值
约束
key.value不允许为空,因为不知道此处到底是存储了值还是没有存储值,即null的含义 -DOUG LEA
比较
1 jdk1.7 以segement为基准,segement继承自ReentrantLock
2 jdk1.7最大并发粒度为16
3 哈希冲突解决为链表,1.8 为了解决链表遍历复杂度为o(n),当链表长度超过8时转变为红黑树
4 jdk1.7链表采用头插法 jdk1.8 采用尾插法
CELLSBUSY
0 初始值
1 contcell 在扩容或者初始化
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
volatile long value;
CounterCell(long x) { value = x; }
}
缓存行填充
疑问点
countcell在扩容时,另一个线程对原数组的某一个值进行cas累加,是否会导致数据可见性问题
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break; // 线程1
// 线程2
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
break; // 线程1
// 线程2
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
拓展知识点
volatile 的数组只针对数组的引用具有
volatile 的语义,而不是它的元素
volatile 的语义,而不是它的元素
并发基础
cas
compare and swap (cpu层面的原子指令)
底层实现
旧的内存值,预期值,要更新的值,当且仅当内存值与预期值一致时才进行更新
native中存在4个参数
缺陷
cpu空转
aba问题
只能保证一个共享变量原子操作
原子性源头
线程切换 线程执行过程中不被中断
一个个执行,顺序化
调度算法
抢占式
轮询
可见性源头
cpu的运算速度与内存读写的速度天差地别,所以出现了cp缓存
cpu缓存行
缓存行伪共享
伪共享: 当一个CPU高速缓存行发生改变,为了不影响其他CPU的相关数据的正确性,必须通过某些手段让其他拥有该缓存行数据的CPU高速缓存进行数据一致性同步,这就是造成“伪共享”的所在,试想运行在两个不同CPU核心上的两个线程,如果他们操作的内存数据在物理上处于相同或相邻的内存块区域(或者干脆就是操作的相同的数据),那么在将它们各自操作的数据加载到各自的一级缓存L1的时候,在很大概率上它们刚好位于一个缓存行(这是很有可能的,毕竟一个缓存行的大小一般有64个字节),即共享一个缓存行,这时候只要其中一个CPU核心更改了这个缓存行,都会导致另一个CPU核心的相应缓存行失效,如果它们确实操作的是同一个数据变量(即共享变量)这无可厚非,但如果它们操作的是不同的数据变量呢,依然会因为共享同一个缓存行导致整个缓存行失效,不得不重新进行缓存一致性同步,出现了类似串行化的运行结果,严重影响性能,这就是所谓的“伪共享”
解决办法
填充缓存行
@sun.misc.Contended
cpu缓存行 大小64字节
cpu缓存
L1 独占
L2 独占
L3 共享
有序性源头
指令重排序
局部性原理(在CPU访问寄存器时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理)
空间局部性
如果一个存储器的位置被引用,那么将来他附近的位置也会被引用
时间局部性
被引用过一次的存储器位置在未来会被多次引用(通常在循环中)
线程间通讯
共享内存
消息传递
内存模型
volitale语义
作用
可见性
禁止指令重排序
原理
总线上lock锁
缓存锁
MESI 缓存一致性协议
STORE BUFFER
store forwarding
invalid queue
特性
可见性
原子性
仅在32位系统上对long.double的读写具有原子性
实现机制
内存屏障
规范
storestore
Store1;StoreStore;Store2
该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
storeload
store1 ;storeload;load2
该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令
loadload
Load1;LoadLoad;Load2
该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
loadstore
Load1;LoadStore;Store2
确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
x86实现
sfence
Store Barrier,相当于StoreStore Barriers。
lfence
Load Barrier,相当于LoadLoad Barriers。
mfence
Full Barrier ,相当于StoreLoad Barriers。
final
如果一个实例的字段被声明为final,则JVM会在初始化final变量后插入一个sfence
内存语义
读
将线程本地内存的共享变量对应的缓存行失效,直接从主内存中读取
写
当写一个volitale共享变量时,会从工作内存刷新到主内存中
指令重排序
优化点
并行指令集重排序
在处理器内核中一般会有多个执行单元,比如算术逻辑单元、位移单元等。
在引入并行指令集之前,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;
在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。
在引入并行指令集之前,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;
在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。
对于一条从内存中读取数据的指令,CPU的某个执行单元在执行这条指令并等到返回结果之前,按照CPU的执行速度来说它足够处理几百条其他指令,而CPU为了提高执行效率,会根据单元电路的空闲状态和指令能否提前执行的情况进行分析,把那些指令地址顺序靠后的指令提前到读取内存指令之前完成
内存系统指令重排序
重排序规则
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前
当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后
.当第一个操作为volatile写时,第二个操作为volatile读时,不能重排
jmm对volatile变量操作规约
volatile写
在volatile写前面添加store store指令
在volatile写后面添加storeload 指令
volatile 读
volatile读后添加LoadLoad指令
volatile读后添加LoadStore指令
happens-before
程序的顺序性规则
volitale语义
传递性规则
管程锁规则
线程join规则
线程start规则
as-if-serial
单线程执行结果不被改变
计算机操作系统中的局部性原理
时间局部性
程序有在一段时间内多次访问同一个数据块的倾向
空间局部性
程序往往有访问一个聚集空间的数据块的倾向,参见数组的访问
线程基础
线程状态
NEW
RUNNABLE
WAITING
join.sleep.wait.await
LockSupport.park() 不会抛出中断异常 但会唤醒等待的线程
TIMED_WAITING
BLOCK
synchronized
TERMINATED
线程中断
interrupt
interrupted
中断线程 通过信号通知,外部发出通知,线程响应这个通知
示例:
selector 并未进入WAITING,处于Runnable,但是线程已经被中断了
Thread.cpp 源码
join
调用wait()
调用的线程从runnable进入waiting状态
线程优先级
线程上下文切换
时机
线程run方法结束
系统中断
LockSupport.park
LockSupport.unpark
syncronized
存在I/O阻塞, cpu调度器切换到其他线程,避免cpu资源浪费
多个线程抢占synchronized 同步锁资源
原因
cpu时时间片分配
线程挂起
线程恢复
cas指令 cpu层面的指令 不会出现线程中断
查看命令
vmstat 中的cs(content switch 每秒上下文切换次数) 值越小越好
什么是上下文
为什么会发生上下文切换
为了提高cpu的利用率,可以让当前系统运行远多于cpu核数的线程,同时运行的线程数由cpu核数决定,所以cpu会为线程分配响应的时间片,
sleep.wait区别
sleep不释放锁.wait释放锁
sleep不释放系统资源.wait释放系统资源,但是两者都会释放cpu执行权
JMM
标准
屏蔽操作系统与程序的隔离,提供一个规范
每个线程存在一个工作内存(线程私有),主内存(共享),每个线程会从主内存中copy自己所需要用到的共享变量到工作内存中,每个线程只能在自己的工作内存中修改共享变量,然后同步到主内存中,禁止直接操作主内存
内存屏障
storestore
storeload
loadload
loadstore
操作系统实现
ifence
sfence
mfence(full fence)
synchronized
锁粗化
锁消除
锁的升级
无锁
偏向锁
轻量级锁
重量级锁
锁相关
问题
死锁
产生的死锁的四个条件
互斥
共享资源 X 和 Y 只能被一个线程占用;
占有且等待
线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
不可抢占
其他线程不能强行抢占线程 T1 占有的资源
循环等待
线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
破坏条件
占有且等待
我们可以一次性申请所有的资源
不可抢占
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
循环等待
可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
活锁
休眠随机时间
锁饥饿
锁的分类
是否阻塞
悲观锁
乐观锁
多个线程能不能共享一把锁
共享锁
排他锁
一个线程中的多个流程能不能获取同一把锁
可重入锁
非可重入锁
锁的升级
不锁住资源,多个线程中只有一个能修改资源成功,其他线程会重试
无锁
同一个线程执行同步资源时自动获取资源
偏向锁
多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放
轻量级锁
多个线程竞争同步资源时,没有获取资源的线程阻塞等待被唤醒
重量级锁
抢占锁是否排队
公平锁
非公平锁
抢占同步资源失败是否阻塞
阻塞
不阻塞
自旋
适应性自旋
特点: 保护共享资源, 互斥
DCL
单例模式
dcl
重排序
happens-before
解决方案
volitale 关键字
禁止重排序
原因
step1 分配空间 step2 初始化, step3 赋值 (步骤可能乱序)
0 条评论
下一页