Java锁机制

1. 乐观锁/悲观锁

参考文章

不可不说的Java“锁”事

1.1. 乐观锁

  • 无锁,认为每次访问共享资源不会冲突
  • 使用CAS保证线程安全性
    • CompareAndSwap,原子操作
    • 3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
    • 当且仅当地址V的值与A相等时,将地址V的值修改为B,否则就什么都不做。
    • CAS失败的线程,可以再次尝试
  • 适合读多写少

1.2. 悲观锁

  • 有锁,认为每次访问共享资源都会冲突
  • 适合写多读少

自旋锁 VS 适应性自旋锁

在这里插入图片描述

2. 独享锁/共享锁

  • 独享锁是指该锁一次只能被一个线程所持有,ReentrantLock和写锁
  • 共享锁是指该锁可被多个线程所持有,读锁

3. 互斥锁/读写锁

ReadWriteLock接口

只有两个方法

  • readLock()
  • writeLock()

ReentrantReadWriteLock是ReadWriteLock接口的实现

4. 可重入锁–一个线程可以多次请求自己持有对象锁的临界资源

4.1. ReentrantLock()

  • 一个线程可以同时加多层锁,但是结束后要释放多层锁
  • 显示锁
    lock.lock();//线程会阻塞于此
    lock.tryLock();//非阻塞式,立即返回锁的获取情况
    // 临界区
    lock.unlock();
    

4.2. synchronized

  • 隐式锁
  • 当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块
synchronizedLock
关键字,JVM底层实现机制接口
自动释放锁不能自动释放锁
非公平锁可以公平也可以不公平
不可以中断可以中断也可以不中断
不能知晓是否拿到锁可以知晓是否拿到锁
不提供读写锁提供读写锁
阻塞式获取可以非阻塞式获取

5. 公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
    在这里插入图片描述
    在这里插入图片描述

6. 分段锁:无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

synchronized关键字内部锁升级机制

6.1. 对象头及Mark Word

6.1.1. 对象头

长度内容说明
32/64bitMark Word存储对象的hashCode锁信息
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果是数组)

6.1.2. Mark Word

内容

无锁

  • CAS
  • 锁升级机制:https://blog.csdn.net/tongdanping/article/details/79647337

6.2. 偏向锁

6.2.1. 设计依据

  1. 大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁
  2. 偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能

6.2.2. 加锁

  1. 线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
  2. 下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己的线程ID
    • 如果,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁
    • 如果不是,检查是否为偏向锁
      1. 如果就代表有另一个线程来竞争这个偏向锁,则尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID
        • 成功,表示之前的线程不存在了,Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
        • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
      2. 如果不是,则自旋等待锁释放

6.2.3. 撤销锁

  • 偏向锁不会主动释放
  • 有竞争出现才会释放
  • 安全点处检测持有偏向锁的线程是否活跃,决定是否退回无锁状态

6.3. 轻量级锁

6.3.1. 设计依据

  • 对于绝大部分的锁,在整个同步周期内都是不存在竞争的

6.3.2. 加锁

在代码进入同步块之前,JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
image.png

  1. 拷贝对象头中的Mark Word复制到锁记录Displaced Mark Word中;
  2. JVM使用CAS操作尝试将锁对象的Mark Word更新为指向Displaced Mark Word的指针,并将线程栈帧中的Displaced Mark Word里的owner指针指向Object的 Mark Word。
    • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
      image.png
    • 如果这个更新操作失败了(mark word存储了其他栈桢的内容),表示其他线程竞争锁,当前线程就尝试使用自旋来获取锁,适应性自旋,自旋次数逐渐减少。

6.3.3. 锁的释放

  • 当前线程使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。
    • 如果成功,说明没有竞争
    • 如果失败,说明存在竞争(其他线程自旋超过一定次数会将mark word修改为重量级锁的状态),锁会膨胀成重量级锁

6.4. 重量级锁

6.5. 锁的优缺点对比

偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行时间较长。

7. 自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

AQS(AbstractQueuedSynchronizer)队列同步器

  • 抽象类
  • 维护一个volatile成员遍历state记录同步状态
  • 内置FIFO队列
  • 同步节点:保存获取同步状态失败的线程引用、等待状态以及前驱节点和后继节点

参考文章

https://my.oschina.net/u/4149877/blog/4298976
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

常见方法

  • acquire-release、
  • acquireShared-releaseShared

Node节点

在这里插入图片描述
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0:新结点入队时的默认状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值