# ReentrantLock

Reentrantlock 是可重入锁,内部实现了公平锁,也实现了非公平锁。

其实之前讲解 AQS 中实现的自定义锁就是一个可重入非公平锁,只不过 ReentrantLock 内部实现了公平与非公平两种锁。

一个新线程调用 tryAcquire 可以直接抢锁,而不需要管阻塞队列中是否有其他线程等待拿锁。这就是非公平的。

image-20220817141354041

ReentrantLock公平实现:

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;
    }

公平锁在 c==0 时加上了对阻塞队列中线程的检查,也就是 hasQueuedPredecessors() 方法

public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法必须要适用于两种线程:

  • 被唤醒的 head 的后继节点:此时在 acquireQueued 函数中会先调用 tryAcquire 再将节点设为头节点。所以此时该节点的线程调用 hasQueuedPredecessors 返回 false ,可以获得锁。
  • 刚准备获取锁的新线程:这里的新指的是线程没有在阻塞队列。该线程在调用 hasQueuedPredecessors 时阻塞队列里面的节点会发生变化。如果 h == t ,说明队列没有初始化或者没有阻塞节点,直接返回 false 。能继续执行代码,说明 h!=t ,按理来说 h.next 是不会为 null 的,这个问题其实是出现在初始化队列时发生的:
if (t == null) { // 第一次入队,没有 dummy node 的存在,需先创建它
    if (compareAndSetHead(new Node()))
        // 执行下面语句时发生中断,导致 head!=tail 且 head.next=null
        tail = head;
}

此时说明有个没拿到锁的线程正在初始化队列,而对于新的线程来说,要保证公平,就不能拿锁。

读者可能疑惑这种情况,所有阻塞节点都出队了,假设最后一个拿到锁的线程为 t,那么 headtail 都指向 t 的节点。

该函数还有细节:也就是先读取 tail 再读取 head

Node t = tail; 
Node h = head;

还是和之前的初始化队列代码一起看:

如果先读取的是 head ,就有可能出现 head==nulltail != null 的情况。同时为了避免重排序, headtail 都被 volatile 修饰。

我个人有个问题就是:如果照着 enq 初始化队列时会中断的思路,那么如果刚执行完 tail=head 然后发生中断,那么此时再有线程 s 执行 hhasQueuedPredecessors ,就会判断 h==t 从而使得函数返回 false ,这不就和本意相违背了吗,明明有个线程正在进行入队操作,凭什么抢了对方的锁?

我看网上大多数关于该函数的讲解都没有提到过,彷佛就是避而不谈,当然也可能是我学艺不精,如果读者有什么见解,劳烦发邮箱:laurensvfevaa@gmail.com

# ReentrantReadWriteLock

该锁提供了两种锁:读锁和写锁。是为了适用于多读少写的场景,因为读操作可以共享,实现逻辑为:

  • 获取读锁:如果没有线程获取写锁,就可以获取读锁
  • 获取写锁:既没有读锁,也没有其他线程获得写锁,才可以获取。

读写锁都是可重入的,和 ReentrantLock 一样,可以选择公平或非公平。

public class Cache {
    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    public static Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    public static void put(String key, Object val) {
        w.lock();
        try {
            map.put(key, val);
        } finally {
            w.unlock();
        }
    }
}

# 内部结构

在重写 AQS 方法前,需要解决的是,用一个整型同时记录读状态和写状态,一个整型变量维护多种状态的情况,一定要使用 “按位切割使用” 这个变量,此处设计为高 16 位记录读,低 16 位记录写

写线程获取锁修改 state ,就是正常的 +1 ,可以理解为低 16 位 + 1;而读线程获取锁修改 state ,是高 16 位 + 1,即为每次 s tate+=SHARED_UNIT ,SHARED_UNIT 是一个很大数( 1<<16+1

为了对之后的源码讲解减轻压力,先提前介绍几个函数

  • sharedCount(int c) 函数:标识占有读锁的线程数量
  • exclusiveCount(int c) 函数:表示占有写锁的线程数量

# WriteLock

写锁是独占锁,所以需要重写的是 tryAcquire (当然,我指的是 Sync 重写该函数)

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c); // 写锁
    if (c != 0) { // 存在读锁或者写锁,可能是自己重入锁
        
        // 存在读锁或者写锁被其他线程占了(w=0 而 c!=0 就表示读锁不为 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重入锁,不需要 CAS
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

tryAcquire 的大致流程就是,先判断有没有读锁,再判断当前写锁的独占线程是不是自己,最后判断获取的锁线程数量会不会超过 MAX_COUNT

写锁的释放操作与 ReentrantLock 相似,每次释放减少写状态,当写状态为 0 时表示写锁被释放。

# ReadLock

AQS 除了独占模式,还有共享模式,需要重写 tryReleaseSharedtryAcquireShared 函数。而若干个线程的读操作就可以在共享模式中执行。

获取读锁从 Java5 到 Java6 变得复杂了许多,主要是增加了一些新功能,如 getReadHoldCount() 方法,作用是返回当前线程获取读锁的次数,这种状态只能保存在 ThreadLocal 中,由线程自身维护,使得读锁实现变得复杂。此处给出的代码来自《Java 并发编程的艺术》代码清单 5-18,它删减了部分代码,只保留了必要的部分。

protected final int tryAcquireShared(int unused) {
    for(;;) {
        int c = getState();
        int nextc = c + (1 << 16);
        if(nextc < c) // 高 16 位溢出了
            throw new Error("Maximum lock count exceeded");
        // 存在写锁并且当前线程没拿到写锁
        if(exclusiveCount(c) != 0 && owner != Thread.currentThread())
            return -1;
        // CAS 失败可能是其他线程也在拿读锁,再循环直到获得读锁
        if(compareAndSetState(c,nextc))
            return 1;
    }
}

读锁的释放,不仅要修改 state ,还要修改计数器,也就是 HoldCounter ,这个待会会讲解。

# 理解计数器

Sync 还有两个内部类: HoldCounterThreadLocalHoldCounter

# HoldCounter

// HoldCounter-- 计数器,主要与读锁配套使用
static final class HoldCounter {
    // 计数
    int count = 0;
    // 获取当前线程的 TID 属性的值
    final long tid = getThreadId(Thread.currentThread());
}

count 表示某个线程重入的次数tid 表示该线程的 tid 字段的值,该字段可以唯一标识一个线程。

# ThreadLocalHoldCounter

// 本地线程计数器
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    // 重写初始化方法,在没有进行 set 的情况下,获取的都是该 HoldCounter 值
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

ThreadLocalHoldCounter 重写了 ThreadLocalinitialValue 方法,** ThreadLocal 类可以将线程与对象相关联。** 在没有进行 set 的情况下, get 到的均是 initialValue 方法里面生成的那个 HolderCounter 对象。

# 参考

https://blog.csdn.net/anlian523/article/details/106173860

https://www.cnblogs.com/leesf456/p/5383609.html

https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantLock.html

https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantReadWriteLock.html

https://blog.51cto.com/stefanxfy/5083396#ReentrantReadWriteLock_9

https://www.cmsblogs.com/article/1391297853107867648

《Java 并发编程的艺术》第 5 章