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

4 阅读6分钟

前言

为了方便理解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 包中,其核心实现基于一个抽象框架:

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

静态抽象内部类Sync

/**
 * 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

/**
 * 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

/**
 * 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 ,它判断队列中是否有优先级更高的等待线程:

// 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

特殊说明

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

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

// 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:头节点的下一个节点不是当前线程
}

即主要是这段代码:

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

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

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

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

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<br>队列初始化中]
        N -->|否| P[检查s.thread]
    end
    
    G -->|此时状态:<br>tail已更新为node<br>但t.next尚未设置| Q[竞争窗口]
    Q --> N

在这个竞争窗口中

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

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