Java 线程同步-05:基于Sync抽象类的公平锁和非公平锁

前言

为了方便理解Java Lock的公平锁和非公平锁,这里再回顾下Java Lock相关的类继承结构:

flowchart TD %% 接口层 Lock[Lock接口] --> RL[ReentrantLock] Lock --> RLock[ReadLock] Lock --> WLock[WriteLock] RWL[ReadWriteLock接口] --> RWLImpl[ReentrantReadWriteLock] RWLImpl --> RLock RWLImpl --> WLock Cond[Condition接口] --> CondObj[ConditionObject] %% AQS层 AQS[AQS] --> Sync[Sync] AQS --> RWSync[ReadWriteSync] Sync --> FSync[FairSync] Sync --> NFSync[NonfairSync] %% 组合关系 RL -.-> Sync RWLImpl -.-> RWSync RLock -.-> RWSync WLock -.-> RWSync Sync -.-> CondObj RWSync -.-> CondObj style Lock fill:#f9f style RWL fill:#f9f style Cond fill:#f9f style AQS fill:#ccf style RL fill:#cfc style RWLImpl fill:#cfc style RLock fill:#cfc style WLock fill:#cfc

本文主要分析Java Lock的公平锁和非公平锁定义、源码实现和特殊说明。

什么是公平锁和非公平锁

这两种锁主要针对 获取锁的顺序 而言,主要常见于 ReentrantLock 的实现中。

公平锁

  • 定义:公平锁是指多个线程按照申请锁的顺序来获取锁。线程直接加入等待队列,队列遵循 FIFO(先进先出)原则。当一个线程尝试获取锁时,它会先检查队列中是否有其他线程在等待。如果有,它就会乖乖排到队尾,不会尝试"插队"。
  • 优点:绝对的公平,不会出现线程"饥饿"现象(即某个线程一直拿不到锁)。
  • 缺点:性能相对较低,吞吐量不如非公平锁。

非公平锁

  • 定义:非公平锁是指多个线程获取锁的顺序并不一定按照申请锁的顺序。允许线程"插队"。当一个线程尝试获取锁时,无论队列中是否有等待线程,它都会先尝试直接抢占锁(CAS操作)。如果抢占成功,就获取锁;如果失败,才会进入队列排队。
  • 优点:性能高于公平锁。因为减少了线程挂起和唤醒的开销,提高了系统的整体吞吐量。
  • 缺点:可能导致线程"饥饿",即某个线程运气不好,总是抢不到锁,一直在等待。

为什么非公平锁性能更好?

在公平锁机制下,线程A释放锁后,需要唤醒队列中的线程B。在B被唤醒并真正获取锁的这段时间间隔内,如果有新线程C来了,公平锁会让C排队等待。 而在非公平锁机制下,C可以直接"插队"获取锁。这省去了C排队、挂起和后续唤醒B的开销,充分利用了CPU的时间片。

关键源码

Java的锁机制在 java.util.concurrent.locks 包中,其核心实现基于一个抽象框架:

scss 复制代码
Lock 接口 (例如 ReentrantLock)
       |
       | 依赖
       ↓
Sync 内部类 (继承自 AbstractQueuedSynchronizer,即 AQS)
       |
       | 具体实现
       ↓
公平锁(FairSync) 或 非公平锁(NonfairSync)

静态抽象内部类Sync

java 复制代码
/**
 * Base of synchronization control for this lock. Subclassed
 * into fair and nonfair versions below. Uses AQS state to
 * represent the number of holds on the lock.
 */
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
	    final Thread current = Thread.currentThread();
	    int c = getState();
	    if (c == 0) {  // 锁未被占用
	        if (compareAndSetState(0, acquires)) {  // CAS尝试抢锁
	            setExclusiveOwnerThread(current);    // 成功则设置当前线程为持有者
	            return true;
	        }
	    }
	    else if (current == getExclusiveOwnerThread()) {  // 可重入逻辑
	        int nextc = c + acquires;  // 增加重入次数
	        if (nextc < 0) // overflow
	            throw new Error("Maximum lock count exceeded");
	        setState(nextc);  // 设置新的state,无需CAS(因为已持有锁)
	        return true;
	    }
	    return false;
	}

    protected final boolean tryRelease(int releases) {
	    int c = getState() - releases;  // 计算释放后的state
	    if (Thread.currentThread() != getExclusiveOwnerThread())
	        throw new IllegalMonitorStateException();  // 安全校验
	    boolean free = false;
	    if (c == 0) {  // 完全释放锁
	        free = true;
	        setExclusiveOwnerThread(null);  // 清除锁持有者
	    }
	    setState(c);  // 设置新state
	    return free;  // 返回是否完全释放
	}

	// 判断当前线程是否持有锁
    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

	// 创建与当前锁关联的条件变量
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

    final boolean isLocked() {
        return getState() != 0;
    }

    /**
     * Reconstitutes the instance from a stream (that is, deserializes it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

静态内部类 NonfairSync

java 复制代码
/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires); // 复用父类的非公平获取逻辑
    }
}

关键设计

  1. 两级尝试机制 :先在 lock() 中直接CAS,失败后再通过 acquire() 进行完整尝试
  2. 性能优化:避免了不必要的队列操作,新线程有机会"抢到"刚释放的锁
  3. 饥饿风险:新线程和队列中等待的线程竞争,可能导致等待线程长期获取不到锁

静态内部类 FairSync

java 复制代码
/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

公平锁在获取锁时会先判断当前锁是否有其他等待线程,如果有的话需要进行排队。这里的关键函数是hasQueuedPredecessors ,它判断队列中是否有优先级更高的等待线程:

java 复制代码
// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
    // 读取尾节点和头节点
    Node t = tail;
    Node h = head;
    Node s;
    
    // 判断是否有前驱节点的三种情况:
    return h != t &&  // 队列不为空(头尾不同)
        ((s = h.next) == null ||  // 情况1:队列正在初始化
         s.thread != Thread.currentThread());  // 情况2:头节点的下一个节点不是当前线程
}

公平锁执行流程:

sequenceDiagram participant Thread as 新线程 participant FairSync as 公平锁 participant AQS as AQS队列 participant Lock as 锁状态 Thread->>FairSync: lock() FairSync->>AQS: acquire(1) AQS->>FairSync: tryAcquire(1) alt 锁空闲 FairSync->>AQS: hasQueuedPredecessors() alt 没有前驱节点 FairSync->>Lock: compareAndSetState(0,1) Lock-->>FairSync: 成功 FairSync->>FairSync: setExclusiveOwnerThread() FairSync-->>AQS: return true AQS-->>Thread: 获取成功 else 有前驱节点 FairSync-->>AQS: return false AQS->>AQS: addWaiter() AQS->>AQS: acquireQueued() loop 自旋等待 AQS->>FairSync: tryAcquire(1) FairSync->>AQS: return false AQS->>AQS: parkAndCheckInterrupt() end end else 锁被占用 FairSync->>FairSync: 检查是否重入 alt 是当前线程持有 FairSync->>Lock: state++ FairSync-->>AQS: return true AQS-->>Thread: 重入成功 else 非当前线程持有 FairSync-->>AQS: return false AQS->>AQS: 入队等待 end end

特殊说明

关于公平锁对队列初始化情况的判断

前面介绍公平锁判断是否有前驱节点的时候,判断了队列是否初始化的情况:

java 复制代码
// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
    // 读取尾节点和头节点
    Node t = tail;
    Node h = head;
    Node s;
    
    // 判断是否有前驱节点的三种情况:
    return h != t &&  // 队列不为空(头尾不同)
        ((s = h.next) == null ||  // 情况1:队列正在初始化
         s.thread != Thread.currentThread());  // 情况2:头节点的下一个节点不是当前线程
}

即主要是这段代码:

java 复制代码
(s = h.next) == null // 情况1:队列正在初始化

hasQueuedPredecessors() 遇到 h.next == null 时,它返回 true,意思是:有前驱节点(或者队列正在初始化,你应该等待)。这个设计是保守的:

  • 宁可让新线程等待(返回true),也不让它错误地认为队列为空
  • 避免在队列初始化过程中出现竞争问题

当线程进入等待队列的时候,会调用如下代码:

java 复制代码
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {  // 阶段1:队列完全为空
            // 创建空节点作为头节点
            if (compareAndSetHead(new Node()))
                tail = head;  // 此时: head = 空节点, tail = 空节点
        } else {  // 阶段2:将新节点添加到队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {  // 步骤A: 设置tail为新节点
                t.next = node;  // 步骤B: 设置prev节点的next指针
                return t;
            }
        }
    }
}

这里会发生并发冲突,上面对初始化判断的case主要会发生在如下场景:

graph TD subgraph "enq() 执行过程" A[开始入队] --> B{t == null?} B -->|是| C[CAS设置head为新建的空节点] C --> D[tail = head] D --> E[循环继续] B -->|否| F[node.prev = t] F --> G[CAS设置tail = node] G --> H[t.next = node] H --> I[返回] end subgraph "hasQueuedPredecessors() 检查" J[读取h = head] --> K[读取t = tail] K --> L{h != t?} L -->|是| M[读取s = h.next] M --> N{s == null?} N -->|是| O[返回true
队列初始化中] N -->|否| P[检查s.thread] end G -->|此时状态:
tail已更新为node
但t.next尚未设置| Q[竞争窗口] Q --> N

在这个竞争窗口中

  • tail 已经指向新节点(线程B的节点)
  • head.next 还没有被设置(t.next = node 尚未执行)
ini 复制代码
时间窗口中的队列状态:
         head               tail
          ↓                  ↓
     [空节点]             [节点B]
         ↑                  ↑
    head.next = null     tail = node

此时 h != t(头尾不同)但 h.next == null,这表示队列正在初始化,新节点还没有完全链接到队列中。

相关推荐
熙胤10 分钟前
Spring Boot 3.x 引入springdoc-openapi (内置Swagger UI、webmvc-api)
spring boot·后端·ui
tumeng071118 分钟前
springboot项目架构
spring boot·后端·架构
LES000LIE21 分钟前
Spring Cloud
后端·spring·spring cloud
mldlds42 分钟前
Spring Boot应用关闭分析
java·spring boot·后端
zjjsctcdl44 分钟前
Spring Boot与MyBatis
spring boot·后端·mybatis
tuyanfei1 小时前
Spring 简介
java·后端·spring
代码探秘者1 小时前
【大模型应用】2.RAG详细流程
java·开发语言·人工智能·后端·python
神奇小汤圆1 小时前
网易一面:KAFKA写入数据时是先写Leader还是先写Follower?
后端
baizhigangqw1 小时前
Spring Boot spring.factories文件详细说明
spring boot·后端·spring