Java多线程:关于锁
互斥访问资源
加锁的本质是,为了竞争一个资源访问互斥状态,保证线程安全
如果只是读,是线程安全的,因为竞态资源不会修改和数据不一致
读不需要互斥,但是读的时候不能,而写需要互斥
互斥状态的要求:atomic、volatile
原子性:互斥状态抢占后不能被中断,需要原子修改互斥状态
可见性:互斥状态必须全局同一,不能有缓存导致的数据不一致
操作系统互斥锁mutex的缺点
在sychronized锁中利用了mutex实现互斥
额外的时间开销 -- 缺点:
- 可以优化的缺点:
- 管态切换,调度线程
- 线程上下文中断、保存与恢复,线程切换
- 线程阻塞
- 线程唤醒
- 无法优化的缺点:
- 并发到串行 --> 无法改变,线程安全,就要加锁
- 线程创建与销毁 --> 优化:线程池-退出从销毁改为循环
偏向锁、轻量级锁、重量级锁
优化思路:
- 不存在线程安全问题(锁消除):
此时不需要加锁
如果加锁jvm会进行"锁消除"的编译层面优化 - 不需要竞争(偏向锁):
此时不需要加锁,但是很难判断有没有竞争 - 不需要阻塞(轻量级锁):
CAS乐观锁 + 自旋- 抢不到锁,先乐观的认为马上就能抢到锁,进行有限次重试
- 如果有限次重试内没有抢到锁,再阻塞
- 乐观的原因:线程任务短,可以很快释放锁
- 如果线程任务长(重量级锁,mutex)
轻量级锁的重试是无用功,此时需要阻塞和唤醒
但是其实相比于长时间的线程任务,阻塞和唤醒的时间就无足轻重了 - 锁升级:
如果当前并发量和任务时长增加,此时需要进行锁升级 - 锁膨胀:编译器增加锁的粒度
乐观锁
乐观锁有两种,CAS,或者版本号,版本号一般是数据库中的乐观锁实现
CAS
CAS是java中的UnSafe类中的native方法,先获取当前时刻的值,之后在下一时刻比较是否发生了变化
底层是操作系统添加了总线锁防止其他线程对共享变量的访问
ABA问题
当前时刻获取了一个值,之后下一个时刻加总线锁比较这一时刻和上一时刻值并交换的时候,有可能上一个线程将值进行修改之后又修改回来
很多时候ABA并不会导致问题,比如我抢一个队列中的值用CAS,
表示当前是否抢占上队头的值
优化ABA:CAS的同时指定版本号
自旋次数过多问题
线程抢占时间过长,这个考虑将CAS自旋锁进行锁升级为互斥锁,
或者只进行有限次自旋重试
减小锁的粒度
ConcurrentHashMap:只锁当前操作的桶位,这样可以并发写入多个桶位
优化:减少锁住的部分,也可以优化锁
问题 - 锁膨胀:假如当前
共享锁-读锁、排他锁/互斥锁-写锁
最开始说过读锁和写锁,这里继续深入
继续优化:如果并发只是为了读数据,不修改资源
那么不需要对读请求串行化,可以并发读,这个就是共享锁
如果当前环境读多写少,那么并发锁设计成共享锁而非互斥锁
避免死锁的锁特性:重入锁
学习ConcurrentHashMap的锁思想
锁粒度
jdk1.7:Segment/分段锁
jdk1.8:锁住头节点
size个数的统计
count+cells数组,cells数组是由CPU核数控制的
抢数组的哪个单元,是由随机数%数组大小获取的
cas竞争一个数组单元,如果cas失败就重新路由cells单元
计算size:count + 累加cells数组
多线程协同扩容
get方法
无锁
put方法
tab是否存在 -> resize延迟初始化
对应桶位是否是空
锁定桶位头节点,比分段锁粒度更小
MySQL锁
分布式锁
结语
加锁是为了线程安全,但是也要考虑性能
根据读写、并发量、任务时长、锁粒度、避免死锁等等进行优化