文章目录
一、锁分类
1、公平锁与非公平锁
类型 | 描述 | 代表锁 |
---|---|---|
公平锁 | 公平锁就是,当线程执行到临界区时,竞争锁的线程直接进入同步队列进行排队 ,先来的线程可以先获得锁;待到释放锁时,唤醒队列的下一个线程去获取锁; |
ReentrantLock支持公平锁 |
非公平锁 | 非公平锁就是,当线程执行到临界区时,竞争锁的线程直接CAS自旋尝试获取锁 。若能获得锁就直接持有,若自旋一定次数后依旧没有获得锁。则线程进入同步队列进行排队,队列中的线程依旧是按照先进先出的原则获取锁; |
synchronized锁、ReentrantLock支持非公平锁 |
- 非公平锁省避免了线程切换的开销,效率比公平锁要高;
2、独占锁与共享锁
类型 | 描述 | 代表锁 |
---|---|---|
独占锁 | 锁一次只能被一个线程所持有 | synchronized锁、ReentrantLock锁支持独占模式 |
共享锁 | 锁可同时被多个线程所持有 | ReentrantLock锁支持共享模式 |
3、可重入式锁与非可重入式锁
类型 | 描述 | 代表锁 |
---|---|---|
不可重入式锁 | 只判断这个锁有没有被锁上,只要被锁上,申请锁的线程都会被要求等待。 | 继承AQS类,重写tryAcquire()-tryRelease()方法可以实现 |
可重入式锁 | 不仅判断锁有没有被锁上,还会判断锁是哪个线程锁上的。如果是当前线程锁上的,那么它依旧可以再次访问临界资源,并把加锁次数加1。 | ReentrantLock默认就是可重入式锁 |
4、乐观锁与悲观锁
类型 | 描述 | 代表锁 |
---|---|---|
乐观锁 | 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下,在此期间有没有别人去更新这个数据,可以使用版本号机制和CAS算法实现。 | CAS + 版本号机制,提高吞吐量 |
悲观锁 | 每次访问临界区资源都会上锁 | synchronized、ReentrantLock都是悲观锁 |
5、分段锁
HashMap / HashTable / ConcurrentHashMap
数据结构 | 是否线程安全 |
---|---|
HashMap | 线程不安全 ;① put() 和 get()并发时,可能导致get为null; ② 多线程的put()操作可能会导致元素丢失; ③ JDK1.7出现hash冲突时,由于头插法,容易形成环形链表,导致死循环。JDK1.8后用尾插法解决了此问题; ④ 多线程扩容时,易出现死循环。 |
HashTable | 线程安全;HashTable对整个Hash表加了一把大锁,虽然线程安全,但是并发效率极差; |
ConcurrentHashMap | 线程安全; ① JDK1.7时,底层基于是Segment(底层是volatile修饰的hashTable)分段锁机制 + 链表存储结构; ② JDK1.8时,若put()元素时,索引位置没有元素时,采用的是CAS的方式插入;若有元素,则对头节点加锁,然后再去put()元素。此外还有多线程扩容机制,确保不会出现线程安全问题;get()时,无需加锁。因为Node<k,v>的val和next指针都是被volatile修饰的,对其它线程可见; |
ConcurrentHashMap底层原理⭐
//JDK1.7ConcurrentHashMap底层的数据结构
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
// 分段数组,每一段都是一个 hash 表
final Segment<K,V>[] segments;
}
//分段Segment对象,继承了
static final class Segment<K,V> extends ReentrantLock implements Serializable {
// 每段中的表
transient volatile HashEntry<K,V>[] table;
}
- Segment<K,V>对象: 继承了 ReentrantLock,能充当可重入式锁的角色;其实Segment就是hashtable;
- HashEntry<K,V>对象: 被valatile修饰,用于存储键值对数据。
区别 | JDK1.7的ConcurrentHashMap | JDK1.8的ConcurrentHashMap |
---|---|---|
数据结构 | Segmeng数组 + HashEntry数组 + 链表 | 数组 + 链表 + 红黑树 |
底层原理 | 1、调用concurrentHashMap.put(k,v) 方法: ① 当执行 put(k,v) 方法时,若value为null,则直接抛出空指针异常 (因为值为null会有歧义); ② 对传入的key值进行hash,并对Segment的数量取模,获得分段锁的下标; ③ 若Segment不存在,那么调用ensureSegment() 方法创建一个Segment; ④ 然后调用segment.put()方法,开始存放数据; 2、Segment.put()的具体逻辑如下: ⑤ 调用tryLock() 方法尝试获取锁,如果获取锁失败,则调用scanAndLockForPut() 方法获取锁; ⑥ scanAndLockForPut()方法内部死循环式的while(!tryLock())自旋获取锁,最大自旋次数64次,超过后会自动加互斥锁。 在获取锁过程中扫描key,若被其它线程修改过,则重新遍历,自旋次数-1; ⑦ 获取锁成功后,再根据hash值对HashEntry的数量进行取模运算,得到存放数据的HashEntry索引,取出头节点(JDK1.7底层是链表),开始真正的存放数据; ⑧ 遍历链表,若找到了相同的key,则更新值;若没找到相同的key,则使用 头插法 将value插入到链表头节点;⑨ put() 完成后,若需要扩容则进行扩容,并且调用rehash();最后调用unLock()释放锁,完成put(k,v)操作; |
1、调用concurrentHashMap.put(k,v) 方法: ① 当执行 put(k,v) 方法时,底层实际调用的方法是putVal(k,v)方法; ② 对传入的key值进行hash(对Node节点的数量进行取模),得到存放数据的节点位置; 注意: Node<k,v>就是一个Entry,Node为数组 / 链表头节点 / 红黑树根节点; ③ 如果底层数组结构还未初始化,则CAS的初始化; ④ 若hash计算得到的桶位置没有元素,利用CAS的方式插入元素; ⑤ 若hash计算得到的桶位置状态为moved,则表明正在扩容,则调用helpTransfer()方法参与扩容; ⑥ hash计算的桶位置元素不为空,且当前没有处于扩容操作,进行元素添加;具体的,先对当前桶加 synchronized锁 ,保证线程安全的前提下执行添加操作;⑦ 判断当前桶的数据结构: 若是链表: 则使用 尾插法 将value插入到链表尾部;若是红黑树: 那么则将value添加到红黑树; ⑧ 插入完成后,若结构为链表且元素数量 ≥ 8,那么就转为红黑树; ⑨ put() 完成后,判断是否需要扩容;执行完全部逻辑后,synchronized释放锁,完成put(k,v)操作; |
扩容机制 | 在每个Segment内部进行扩容,和HashMap的扩容逻辑类似;先生成新的数组,然后转移元素到新数组中; | JDK1.8有两处扩容时机(底层时机调用的都是transfer()扩容方法): ① 插入元素前,hash到指定的Entry位置时,若Entry状态为Moved,则调用helpTransfer()方法多线程协助扩容,扩容后再添加元素; ② 插入元素后,若达到扩容阈值,则调用addCount()方法扩容; |
二、Synchronized锁
关键 | 描述 |
---|---|
synchronized 锁类型 |
1、非公平锁: 当线程执行到临界区时,竞争锁的线程直接CAS自旋尝试获取锁。若能获得锁就直接持有。若自旋一定次数后依旧没有获得锁,则线程进入同步队列进行排队获取锁; 2、可重入锁: synchronized可重入特性依托于Monitor对象中的计数器,若该线程获取当前锁后,再一次请求当前锁时,允许获取,并且Monitor中的计数器count+1; |
synchronized 底层原理 |
1、修饰代码块(显式同步): 对指定对象加锁; 底层通过MonitorEnter指令 与MonitorExit指令 来管理Monitor对象的获取与释放;在JVM中,Monitor是由ObjectMonitor实现的;它的底层有两个队列,分别是两个双向链表EntryList和WaitSet,分别用于管理等待获取锁的线程与阻塞的线程;① 当多个线程同时访问同步代码块时,由于是非公平锁,所有线程都先去尝试自旋获取锁; ② 达到最大自旋次数后,依然没有获得锁的线程会进入EntryList队列进行排队等待锁的释放; ③ 当持有锁的线程释放Monitor对象后,按照先进先出的原则,EntryList队首线程将获得锁并持有Monitor对象,进入owner区域,并把Monitor的owner属性设置为当前线程,并把计数器count+1; ④ 若线程调用了wait()方法,那么该线程将释放持有的Monitor对象,并将owner变量恢复为null,计数器count-1,同时该线程进入WaitSet队列(双向链表)等待被唤醒; ⑤ 待到当前线程执行完毕,则释放Monitor对象并重置计数器count,此时其它线程可继续抢锁; 2、修饰方法(隐式同步): synchronized修饰方法时,底层并不会通过MonitorEnter指令与MontiorExit指令来进行Monitor对象的管理,而是会给该方法标记 acc_synchronized 字段,表明这是一个同步方法。当方法调用时,JVM会检查方法表结构中的「acc_synchronized」字段是否被设置。若被设置了,则执行线程会先去获取Monitor对象,然后再执行方法。待到方法执行完成后,释放Monitor对象。 ① 修饰实例方法时,对当前实例对象加锁;方法加标识后获取当前实例对象的Monitor; ② 修饰静态方法时,对当前类对象加锁;方法加标识后获取当前类对象的Monitor; |
synchronized 锁升级 |
无锁 → 偏向锁 → 轻量级锁 → 重量级锁; ① 偏向锁: 不存在多线程竞争,锁总是被同一个线程获得;主要是根据对象头MarkWord字段中记录的偏向线程ID实现的; ② 轻量级锁: 存在第二个线程申请锁,但不存在竞争。交替持有锁时,锁升级为轻量级锁,此时「Mark Word」字段指向持有锁的线程的「LockRecord」;在轻量级锁下,其它线程将会基于「自旋」的形式尝试获取锁,而「不会阻塞」; ③ 重量级锁: 存在多线程竞争锁,则升级为重量级锁;此时Mark Word字段记录持有对象监视器Monitor的线程;在重量级锁下,其它线程将会进入等待队列EntryList,等待锁的释放; |
synchronized 锁优化 |
① 锁升级: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁; ② 锁消除: JIT编译优化,自动去掉那些不可能存在竞争的锁; ③ 锁粗化: JIT编译优化,避免重复性的加锁与释放锁,比如for循环内部加锁,可以移到for循环外部; ④ 自旋机制: 基于CAS的自旋锁与自适应锁; |
1、对象的内存布局
- Monitor对象的位置对象的对象头区域中的Mark Word字段里,
2、Monitor对象详解
ObjectMonitor对象结构
//结构体如下
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0; //计数器
_waiters = 0,
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
三、AQS锁框架
关键 | 描述 |
---|---|
核心思想 | 总的来说: AQS就是基于底层的同步等待队列,获取volatile修饰的共享变量state,线程通过CAS自旋的改变状态符,若改变成功则获取锁成功,失败则进入同步等待队列进行排队与释放; ① AQS全称为AbstractQueuedSynchronizer(抽象队列同步器),它是基于 模板方法 的锁同步框架;② AQS的底层维护了一个用volatile修饰的int类型的 state状态字段 ,用于表示当前共享资源的状态;获取共享资源时是基于CAS的方式进行的;③ 它支持 独占式 与共享式 两种获取资源的方式,并基于此开放了4个模板方法 与1个获取资源状态的方法,用于资源的获取与释放;AQS可以通过重写模板方法中对共享资源的获取与释放逻辑,来实现公平锁 与可重入锁 ;④ 对于获取不到共享资源的线程,AQS提供了一个双向链表结构的 同步队列 进行管理;对于调用了await()方法而阻塞的线程,AQS提供了一个单向链表结构的条件队列 进行管理; |
资源共享方式 | AQS定义了两种资源共享方式:独占式、共享式;1、独占式: 同一时刻只能有一个线程能够获取共享资源,比如ReentrantLock锁。独占式锁又可以分为公平锁 / 非公平锁。对应模板方法为: tryAcquire() 和 tryRelease() ; ① 公平锁: 线程进入同步队列排序,按先后顺序获取锁; ② 非公平锁: 线程无视同步队列顺序,直接自旋抢锁。达到最大自旋次数后,进入同步等待队列按顺序获取锁。 2、共享式: 同一时刻可以有多个线程获取共享资源,比如信号量 (Semaphore) 和 循环栅栏 (CountDownLatch) 等;对应模板方法为: tryAcquireShared() 和 tryReleaseShared() ; |
同步队列 | 1、进入同步队列时机: 当共享资源被某个线程占有后,其它请求该资源的线程会被阻塞,进入同步队列排队等待。AQS的底层维护了一个双向链表结构的同步队列,它是一个虚拟队列。不存在队列实例,只存在节点之间的关联关系。 2、Node节点: AQS将每个请求共享资源的线程信息封装成一个Node节点来实现锁的分配与释放。Node节点有四种状态: 只有状态码小于0的节点才需要被唤醒。 ① Canceled = 1 (取消): 表示当前节点从同步队列中取消;自旋获取锁出现异常时改为此状态; ② Signal = -1 : 表示后继节点的线程处于等待状态,如果当前节点释放同步状态则会通知后继节点,使得后继节点能够运行;③ Condition = -2: 表示当前节点的线程调用了await()方法,正在条件队列中等待被signal()唤醒,重新变成Signal状态; ④ Propagate = -3: 表示下一次共享式同步状态的获取将会无条件传播下去; |
1、独占式-获取与释放锁的逻辑
关键 | 描述 |
---|---|
独占式获取锁逻辑 tryAcquire() |
Step1: 当某个线程共享资源时,首先CAS抢锁,抢锁失败后调用AQS的模板方法acquire()方法,底层调用的tryAcquire()方法,该方法会判断当前的state字段是否为0;若为0则说明没有线程持有锁,此时直接CAS直接拿到锁,执行同步代码块; Step2: 若CAS获取锁失败,则首先判断当前线程是否持有锁;若持有锁,则根据可重入性更新state字段的值。然后获取到锁,执行同步代码块。若持有锁的不是当前线程,则调用addWaiter()方法将当前线程封装成一个Node节点,方法内部以自旋的形式保证Node节点能够加入同步队列尾部成功; Step3: 入队操作成功后,执行acquireQueued()方法; ① 若自旋过程发现当前节点的前驱节点是头节点,则此时自旋式的调用tryAcquired()方法,获取锁成功后,将当前线程Node设置为头节点,表示拿到了锁,此时能够访问共享资源; ② 若自旋获取锁的过程中出现异常,则调用cancelAcquire()方法,将当前线程Node节点的状态改为Canceled; ③ 若自旋的过程获取同步状态 (锁) 失败,则从当前线程Node节点开始,向前寻找第一个不为Conceled状态的节点作为前驱节点,并将其的节点状态设置为Signal。然后执行park()操作,阻塞当前线程Node,等待前驱节点为头节点时,才唤醒它调用 tryAcquired() 方法开始抢锁; Step4: 由于AQS的acquire()方法对线程中断不敏感,因此在抢锁等待锁的过程中,如果线程被中断,同步队列不会移除它。因此,AQS底层设置了一个标识位,用于记录线程在同步队列等待过程中是否被中断过。因此,在acquireQueued()执行结束后,若发现这个标识位为true,则表示线程中断过,此时要调用selfInterrupt()方法使得线程恢复中断状态; 至此,获取锁的acquire()逻辑结束。 |
独占式释放锁逻辑 tryRelease() |
Step1: 当某个线程释放锁时,首先会调用release()方法,底层调用AQS中的tryRelease()方法,逻辑很简单,就是将CAS的将state字段恢复为0。 Step2: 锁释放成功后,找到当前节点后第一个状态小于0的节点,调用unparkSuccessor()方法唤醒该节点对应的线程,通知它去自旋的获取锁。 |
2、共享式-获取与释放锁的逻辑
关键 | 描述 |
---|---|
共享式获取锁 逻辑 acquireShared() |
Step1: 当锁的类型是共享式时,首先会调用的acquireShared()方法,AQS底层调用的时tryAcquireShared()方法,若state字段的值大于0,则获取锁成功; Step2: 若获取锁失败,则调用doAcquireShared()方法。该方法会将当前线程信息封装成一个Node节点,调用addWaiter()方法将Node节点添加到同步队列的队尾; Step3: 入队成功后,在doAcquireShared()方法内部自旋式的判断其当前节点的前驱节点的状态,如果前驱节点的状态为头节点,那么就调用tryAcquireShared()方法尝试获取锁。 ① 若获取锁成功,就将当前节点设置为头节点,同时可以访问共享资源。 ② 若自旋获取锁的过程中出现异常,则调用cancelAcquire()方法,将当前线程Node节点的状态改为Canceled; ③ 若自旋获取同步状态 (锁) 失败,那么就向前寻找第一个状态不为Conceled的节点作为前驱节点,将其状态设置为Signal,然后调用park()方法阻塞当前线程,等待前驱节点为头节点时,才唤醒它调用 tryAcquireShared()方法开始抢锁; 至此,获取锁的acquireShared()逻辑结束。 |
共享式获取锁 逻辑 releaseShared() |
Step1: 当某个线程释放锁时,首先会调用releaseShared()方法,底层调用AQS中的tryReleaseShared()方法,逻辑很简单,就是将CAS的将state字段恢复初值。 Step2: 锁释放成功后,调用doReleaseShared()方法,找到当前节点后第一个状态小于0的节点,调用unparkSuccessor()方法唤醒该节点对应的线程,通知它去自旋的获取锁。 |
本文含有隐藏内容,请 开通VIP 后查看