并发编程-Java并发工具类

73 阅读5分钟

我们平时在开发中经常会导入一个包,也就是java.util.concurrent.locks 包,而这个包就是Java并发编程的核心扩展,,提供了比内置synchronized或者valatile关键字更为灵活、强大的锁机制。我们这篇文章就来讲一下这个包下的比较重要的类和接口。

1. ReentrantLock

ReentrantLock是Java并发包java.util.concurrent.locks 下的一个显示锁,什么是显示锁呢?我们来看一下具体的用法:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 确保最终释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

我们可以看到,这里我们显示的调用lock,并且最后unlock,这是ReentrantLock锁的特点之一,且这个锁必须要使用在try/finally里以防锁没办法释放,

我们来看一下,ReetrantLock的源码是怎么定义的:

image.png

整个ReentrantLock继承了Lock锁,而真正的锁实现其实是sync

image.png

而内部实现则是AbstractQueuedSynchronizer,而AQS主要来做线程排队、状态管理、唤醒等逻辑。AQS维护了一个同步状态state(int),以及一个CLH(双向列表)等待队列。然后通过模版方法模式,让子类决定加锁/解锁逻辑。

我们来看一下加解锁的实现: 首先:

public void lock() {
    sync.lock();
}
final void lock() {
    if (compareAndSetState(0, 1))  // CAS 尝试加锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 加锁失败则进入 AQS 队列
}

在这里就是非公平锁的实现,我们可以看到,首先通过CAS尝试加锁,加锁失败则进入AQS队列。然后setExclusiveOwnerThread是记录当前占用锁的线程。那进入AQS后是如何去唤醒呢?我们来看一下AQS处理释放逻辑:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 唤醒队列中的下一个线程
        unparkSuccessor(head);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException(); // 非持有线程不能释放锁

    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

当state递减为0的时候代表完全释放;唤醒AQS队列中等待的下一个线程。

2. ReadWriteLock 和 ReentrantReadWriteLock:读写分离场景

ReadWriteLock锁是读写锁,而ReentrantReadWriteLock是其实现类。在此Java并发包中专为读多写少场景来设计的高性能锁机制,支持读写分离:主要思想就是多线程同时读、写操作必须独占锁(任何读写都互斥)。

用起来很容易,但重要的是具体实现是如何实现的:

2.1 核心设计

2.1.1 状态表示

我们一般使用单个32位整数来表示读写锁的状态:

  • 低16位:写锁计数(可重入次数)
  • 高16位:读锁持有线程数(不是重入次数)
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT); // 0x00010000
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; // 0x0000FFFF
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 0x0000FFFF

// 计算读锁数量
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

// 计算写锁重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

2.1.2 读锁计数优化

从上面我们可以看到。读写锁的计数其实占挺大的开销,这里我们使用特殊优化:

// 第一个获取读锁的线程
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

// 最近一个获取读锁的线程(缓存)
private transient HoldCounter cachedHoldCounter;

// 其他线程的读锁计数
private transient ThreadLocalHoldCounter readHolds;

static final class HoldCounter {
    int count = 0;
    final long tid = Thread.currentThread().getId();
}

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

2.2 核心实现Sync类

2.2.1 写锁获取()

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c); // 写锁计数
    
    if (c != 0) { // 锁被占用
        // 情况1: 有写锁但当前线程不是持有者 → 失败
        // 情况2: 有读锁(写锁为0但有读锁)→ 失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        // 写锁重入检查
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    
    // writerShouldBlock 实现公平/非公平策略
    if ((w == 0 && writerShouldBlock()) ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    setExclusiveOwnerThread(current);
    return true;
}

2.2.2 读锁获取

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    
    // 写锁被其他线程持有 → 失败
    if (exclusiveCount(c) != 0 && 
        getExclusiveOwnerThread() != current)
        return -1;
    
    int r = sharedCount(c); // 读锁数量
    
    // 读锁获取策略(公平/非公平)
    if (!readerShouldBlock() && 
        r < MAX_COUNT && 
        compareAndSetState(c, c + SHARED_UNIT)) {
        
        // 读锁计数管理优化
        if (r == 0) { // 第一个读锁
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { // 第一个线程重入
            firstReaderHoldCount++;
        } else { // 其他线程
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1; // 获取成功
    }
    // 快速路径失败,进入完整版本
    return fullTryAcquireShared(current);
}

2.2.3 公平性实现

公平锁 (FairSync) 与非公平锁 (NonfairSync) 的主要区别在阻塞策略:

// 公平锁实现
static final class FairSync extends Sync {
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors(); // 检查是否有前驱节点
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors(); // 检查是否有前驱节点
    }
}

// 非公平锁实现
static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
        return false; // 写锁总是可以尝试获取
    }
    final boolean readerShouldBlock() {
        // 避免写锁饥饿:检查队列头节点是否是写锁
        return apparentlyFirstQueuedIsExclusive();
    }
}

2.3 锁降级实现

锁降级是 ReentrantReadWriteLock 的重要特性:

// 锁降级示例
void processData() {
    writeLock.lock();
    try {
        // 修改数据...
        
        // 获取读锁(锁降级开始)
        readLock.lock(); 
    } finally {
        writeLock.unlock(); // 释放写锁,降级为读锁
    }
    
    try {
        // 读取数据(仍持有读锁保护)
    } finally {
        readLock.unlock();
    }
}

实现原理:

当线程持有写锁时,可以获取读锁(tryAcquireShared 允许)

释放写锁后,读锁继续保持

保证了数据修改后的一致视图

3. 总结

其实此包里还有一些东西,例如Condition对象,这里就不赘述了,实际上Condition是配合lock使用的一种线程协作工具。这篇文章主要说了其他的锁,其实目的是让大家有更多认识,介绍更加灵活的锁机制,帮助读者在复杂的场景中替代synchronized。