synchronized

偏向锁

当对象第一次作为被获取锁对象时,线程获取锁时发现对象头存储ID为空,就会获取到偏向锁,并且JVM会将线程ID写入对象的对象头中,当下一次这个线程再来的时候,ID匹配成功,就能直接执行不用再获取锁。在线程死亡或退出时批量清理时偏向锁会被释放。

轻量级锁

当第二个线程尝试获取锁时,发现对象已偏向线程A,JVM会先撤销偏向锁,然后线程B进入CAS自旋尝试获取轻量级锁,如果此时该线程仍在代码块中执行会升级为轻量级锁,如果已经执行完则会变为无锁状态。当线程CAS其他对象,轻量锁就会被释放。

重量级锁

当有多个线程去访问到同一个对象时,第一个对象会先抢到锁并开始执行,之后的线程发现获取不到锁,就会开始CAS(自旋),不停循环获取锁,如果循环几次成功了,会获取到轻量锁并开始执行,但当次数到一定程度(或自旋线程数超过CPU核数一半)还是获取不到,JVM会认为竞争激烈,就会把正在执行对象操作的线程的锁从轻量锁膨胀为重量锁,其余线程变为阻塞状态,当线程执行完唤醒后面队列中的线程时重量锁就会被释放。

ReentrantLock

AQS 核心(state、CLH队列、acquire)

ReentrantLock继承自AQS,是独占锁,同时只能有一个线程可以拥有锁。

当第一个线程向对象获取锁成功后,其会将对象的state的值改成1,不会进入CLH队列,之后再来线程尝试获取锁失败,他会CAS一两次失败后进入CLH队列中为head.next,此时它会去查询自己的前驱,发现是head并且为空,就会再次CAS一两次,如果再次失败就会休眠,陷入阻塞状态,之后再来线程获取锁失败,CAS失败,进入队列发现前驱不是head或head被占会直接开始休眠。之后第一个线程执行完把对象的state改为0,并唤醒CLH队头的线程去获取锁。

acquire就是上述流程中管理队列的东西。

AQS 核心流程(上面总结)

  1. 持锁:首个线程 CAS 将 state 改为 1,持锁执行,不进入 CLH 队列

  2. 入队挣扎:后续线程 CAS 尝试失败,包装为 Node 入队(CLH)。

  3. 队首特权:处于 head.next 的节点会再次 CAS 尝试抢锁;若失败,则标记前驱并阻塞休眠

  4. 队尾沉睡:更后面的节点发现前驱非 Head,直接进入阻塞休眠(不尝试 CAS)。

  5. 唤醒接力:持锁线程执行完将 state 改为 0,唤醒 head.next 节点继续竞争。

  6. acquire 职责:它是串联上述 "尝试-入队-阻塞" 流程的标准模板方法。

ReentrantLock的公平锁和非公平锁

公平锁:线程在进行排队时,如果有新线程进来它会查看CLH是否为空,如果不为空即使此时对象的state已经为0可以去获取锁也会进入队列开始排队。

非公平锁:新线程来时如果对象state值为1会先进行CAS几次失败就进入队列排队,如果state的值为0就会直接去争抢锁,此时队列中的第一个线程也会被唤醒并去争夺锁,如果新来的线程抢到就会直接把state改为1开始工作,如果抢不到就会进入队列排队。

ReentrantLock可重入的实现

为什么要可重入:当一个线程进入方法A获取了锁,在方法A中调用了方法B,此时线程需要获取一个与方法A相同的锁,如果不可重入,此时线程已经有了锁无法获取锁,就会在这里陷入死锁。

ReentrantLock的底层依赖维护了exclusiveOwnerThread和state两个关键字段。

字段 含义
state 计数器,记录锁被重入的次数
exclusiveOwnerThread 记录当前持有锁的线程是谁

有了这两个字段AQS内部就知道线程已经有了这个锁,对线程放行并对state++,出来方法B的时候再--直到线程完全从方法A中出来,state变为0锁被释放。

Condition(await/signal)

Condition中的await方法是让一个线程挂起,signal是转移队列。

Condition 是AQS中的一个组件,当一个消费者线程去获取锁但发现其不满足对象执行的条件时,它会被await并进入条件队列开始休眠,只有当有生产者线程去对象中执行使消费者可以满足条件时消费者线程才会被从条件队列移动到等待队列(注意此时并没有被唤醒),当其在等待队列的队头时才会被唤醒去执行。

ReentrantReadWriteLock

ReentrantReadWriteLock也继承自AQS,但ReentrantReadWriteLock是读写锁,它内部维护了两种锁,读锁和写锁。
读锁可以和读锁同时存在,但不能与写锁同时存在,写锁也不能与写锁同时存在。其主要是为了保证读到的数据都相同。

ReentrantReadWriteLock的实现主要通过它的三个内部类 sync,ReadLock,WriteLock。

sync:继承自AQS,负责实现state的位分割,锁的获取和释放逻辑,以及公平和非公平锁(state是一个32位的int类型,sync 会把它分割成两半,高16位记录读锁的线程数,低16位记录写锁的重入次数)。

读锁允许多个线程同时持有,所以需要记录每个线程重入了多少次。如果每次都用 ThreadLocal 去查,性能开销较大。Sync 做了两个巧妙的优化:

优化点 变量 作用
缓存最后一个读线程 cachedHoldCounter 记住最近一次成功获取读锁的线程及其计数。连续读操作时直接命中缓存,避免 ThreadLocal 查找。
记录第一个读线程 firstReader + firstReaderHoldCount 如果只有一个线程在读(无竞争),直接用这两个变量记录,实现 O(1) 计数,完全不走 ThreadLocal

只有当上述两个优化都不命中时,才会回退到 ThreadLocalHoldCounter(继承 ThreadLocal)去获取当前线程的计数。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐