目录
1. 乐观锁/悲观锁
参考文章
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)同步代码块
synchronized | Lock |
---|---|
关键字,JVM底层实现机制 | 接口 |
自动释放锁 | 不能自动释放锁 |
非公平锁 | 可以公平也可以不公平 |
不可以中断 | 可以中断也可以不中断 |
不能知晓是否拿到锁 | 可以知晓是否拿到锁 |
不提供读写锁 | 提供读写锁 |
阻塞式获取 | 可以非阻塞式获取 |
5. 公平锁/非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
6. 分段锁:无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
synchronized关键字内部锁升级机制
6.1. 对象头及Mark Word
6.1.1. 对象头
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果是数组) |
6.1.2. Mark Word
无锁
- CAS
- 锁升级机制:https://blog.csdn.net/tongdanping/article/details/79647337
6.2. 偏向锁
6.2.1. 设计依据
- 大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁
- 偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能
6.2.2. 加锁
- 线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
- 下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己的线程ID
- 如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁
- 如果不是,检查是否为偏向锁
- 如果是就代表有另一个线程来竞争这个偏向锁,则尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID
- 成功,表示之前的线程不存在了,Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
- 如果不是,则自旋等待锁释放
- 如果是就代表有另一个线程来竞争这个偏向锁,则尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID
6.2.3. 撤销锁
- 偏向锁不会主动释放
- 有竞争出现才会释放
- 安全点处检测持有偏向锁的线程是否活跃,决定是否退回无锁状态
6.3. 轻量级锁
6.3.1. 设计依据
- 对于绝大部分的锁,在整个同步周期内都是不存在竞争的
6.3.2. 加锁
在代码进入同步块之前,JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
- 拷贝对象头中的Mark Word复制到锁记录Displaced Mark Word中;
- JVM使用CAS操作尝试将锁对象的Mark Word更新为指向Displaced Mark Word的指针,并将线程栈帧中的Displaced Mark Word里的owner指针指向Object的 Mark Word。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
- 如果这个更新操作失败了(mark word存储了其他栈桢的内容),表示其他线程竞争锁,当前线程就尝试使用自旋来获取锁,适应性自旋,自旋次数逐渐减少。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
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:新结点入队时的默认状态。