返回顶部

Java多线程:关于锁

互斥访问资源

加锁的本质是,为了竞争一个资源访问互斥状态,保证线程安全
如果只是读,是线程安全的,因为竞态资源不会修改和数据不一致
读不需要互斥,但是读的时候不能,而写需要互斥

互斥状态的要求:atomic、volatile

原子性:互斥状态抢占后不能被中断,需要原子修改互斥状态
可见性:互斥状态必须全局同一,不能有缓存导致的数据不一致

操作系统互斥锁mutex的缺点

在sychronized锁中利用了mutex实现互斥
额外的时间开销 -- 缺点:

  • 可以优化的缺点:
    1. 管态切换,调度线程
    2. 线程上下文中断、保存与恢复,线程切换
    3. 线程阻塞
    4. 线程唤醒
  • 无法优化的缺点:
    1. 并发到串行 --> 无法改变,线程安全,就要加锁
    2. 线程创建与销毁 --> 优化:线程池-退出从销毁改为循环

偏向锁、轻量级锁、重量级锁

优化思路:

  1. 不存在线程安全问题(锁消除):
    此时不需要加锁
    如果加锁jvm会进行"锁消除"的编译层面优化
  2. 不需要竞争(偏向锁):
    此时不需要加锁,但是很难判断有没有竞争
  3. 不需要阻塞(轻量级锁):
    CAS乐观锁 + 自旋
    • 抢不到锁,先乐观的认为马上就能抢到锁,进行有限次重试
    • 如果有限次重试内没有抢到锁,再阻塞
    • 乐观的原因:线程任务短,可以很快释放锁
  4. 如果线程任务长(重量级锁,mutex)
    轻量级锁的重试是无用功,此时需要阻塞和唤醒
    但是其实相比于长时间的线程任务,阻塞和唤醒的时间就无足轻重了
  5. 锁升级
    如果当前并发量和任务时长增加,此时需要进行锁升级
  6. 锁膨胀:编译器增加锁的粒度

乐观锁

乐观锁有两种,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锁

分布式锁

结语

加锁是为了线程安全,但是也要考虑性能
根据读写、并发量、任务时长、锁粒度、避免死锁等等进行优化

posted @ 2023-07-19 19:09  你好,一多  阅读(23)  评论(0编辑  收藏  举报