目录
- 互斥访问资源
- 互斥状态的要求:atomic、volatile
- 操作系统互斥锁mutex的缺点
- 偏向锁、轻量级锁、重量级锁
- 减小锁的粒度
- 共享锁-读锁、排他锁/互斥锁-写锁
- 避免死锁的锁特性:重入锁
- 学习ConcurrentHashMap的锁思想
- 结语
互斥访问资源
加锁的本质是,为了竞争一个资源访问互斥状态,保证线程安全
如果只是读,是线程安全的,因为竞态资源不会修改和数据不一致
读不需要互斥,但是读的时候不能,而写需要互斥
互斥状态的要求:atomic、volatile
原子性:互斥状态抢占后不能被中断,需要原子修改互斥状态
可见性:互斥状态必须全局同一,不能有缓存导致的数据不一致
操作系统互斥锁mutex的缺点
在sychronized锁中利用了mutex实现互斥
额外的时间开销 -- 缺点:
- 可以优化的缺点:
- 管态切换,调度线程
- 线程上下文中断、保存与恢复,线程切换
- 线程阻塞
- 线程唤醒
- 无法优化的缺点:
- 并发到串行 --> 无法改变,线程安全,就要加锁
- 线程创建与销毁 --> 优化:线程池-退出从销毁改为循环
偏向锁、轻量级锁、重量级锁
优化思路:
- 不存在线程安全问题(锁消除):
此时不需要加锁
如果加锁jvm会进行"锁消除"的编译层面优化 - 不需要竞争(偏向锁):
此时不需要加锁,但是很难判断有没有竞争 - 不需要阻塞(轻量级锁):
CAS乐观锁 / 自旋锁- 抢不到锁,先乐观的认为马上就能抢到锁,进行有限次重试
- 如果有限次重试内没有抢到锁,再阻塞
- 乐观的原因:线程任务短,可以很快释放锁
- 如果线程任务长(重量级锁,mutex)
轻量级锁的重试是无用功,此时需要阻塞和唤醒
但是其实相比于长时间的线程任务,阻塞和唤醒的时间就无足轻重了 - 锁升级:
如果当前并发量和任务时长增加,此时需要进行锁升级 - 锁膨胀:编译器增加锁的粒度
减小锁的粒度
ConcurrentHashMap:只锁当前操作的桶位,这样可以并发写入多个桶位
优化:减少锁住的部分,也可以优化锁
问题 - 锁膨胀:假如当前
共享锁-读锁、排他锁/互斥锁-写锁
最开始说过读锁和写锁,这里继续深入
继续优化:如果并发只是为了读数据,不修改资源
那么不需要对读请求串行化,可以并发读,这个就是共享锁
如果当前环境读多写少,那么并发锁设计成共享锁而非互斥锁
避免死锁的锁特性:重入锁
学习ConcurrentHashMap的锁思想
结语
加锁是为了线程安全,但是也要考虑性能
根据读写、并发量、任务时长、锁粒度、避免死锁等等进行优化