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,这表示队列正在初始化,新节点还没有完全链接到队列中。

相关推荐
漫霂2 小时前
WebSocket入门
后端·websocket
笨蛋不要掉眼泪2 小时前
Spring Cloud Gateway 核心篇:深入解析过滤器(Filter)机制与实战
java·服务器·网络·后端·微服务·gateway
chentao1062 小时前
Spring应用事件机制实践
后端
序安InToo2 小时前
第4课|程序结构与编译流程
后端·操作系统·嵌入式
名字还在想2 小时前
SpringBoot 自动装配-自定义Stater
后端
茶杯梦轩2 小时前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
服务器·后端·面试
风象南2 小时前
终于找到了!这个开源框架让 AI 真正融入开发流程
后端
Java面试题总结3 小时前
Go-依赖注入
开发语言·后端·golang
Java面试题总结3 小时前
Go 泛型中的 [0]func(T)
开发语言·后端·golang