JAVA并发包(四):ReentrantReadWriteLock读写锁

一、前言

这篇文章主要是源码解读,帮忙理解读写锁里面的难点,也是作为自己对读写锁认识的一个总结。如果要掌握读写锁,看博客是远远不够的,必须回到jar包去品读源码,有不懂的地方再来看博客的注释部分。

二、概述

ReentrantReadWriteLock就是可重入的读写锁,其继承AQS类,内部实现原理自然就是改造过的CLH锁

  1. CLH锁是由Craig, Landin, and Hagersten这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。
  2. CLH锁是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
  3. CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

由CLH锁改良的读写锁可以说是自旋可重入锁,它是可以阻塞的,有公平和非公平两种实现。读写锁有如下特性:

  1. 写锁是独占锁,写锁被占用时,其他线程不能获得写锁和读锁,占用写锁的线程可以获得读锁(锁降级)。
  2. 读锁是共享锁,读锁被占用时,其他线程可以获得读锁。所有线程不能获得写锁。
  3. 加锁数量由32位整数表示。高16位是读锁数量,低16位是写锁数量,读写数各不超过65535个锁

三、读锁

在讲解加锁过程之前,我觉得有必要先来了解一下自旋锁节点的几个状态

CANCELLED = 1:取消状态。表示节点因为超时或者中断被取消。

SIGNAL= -1:信号量。 表示后继节点需要阻塞等待被唤醒(当锁释放或者取消时会唤醒后继节点)

CONDITION = -2:条件状态。表示节点在一个条件队列,在等待某个条件成立。此时该节点不会获取锁,除非条件满足。

PROPAGATE = -3:传播状态。 表示下一次竞争锁失败时可以再次参与竞争锁(这是看源码的自我理解)

0:默认状态。

好了,无须啰嗦,直接上加锁源码

1.读锁加锁过程

public void lock() {
	// 获取共享锁
	sync.acquireShared(1);
}
 
public final void acquireShared(int arg) {
	// 获取读锁,如果小于0表示获取失败,需要执行doAcquireShared自旋获取锁
	 if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

下面看看是如何获取锁的

	// 尝试获取读锁,大于0说明获取成功
	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 {
                	// 这里把获得读锁的信息保存到ThreadLocal,后面判断死锁时需要用到
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // 如果获取锁失败,则进入轮询再次获取
            return fullTryAcquireShared(current);
        }

公平与非公平锁是通过readerShouldBlock方法两种实现来区分的。
非公平读锁

	final boolean readerShouldBlock() {
	 return apparentlyFirstQueuedIsExclusive();
	}
// 如果头节点不为空,并且头节点的下一个节点不为空,并且头节点的下一个节点不是共享模式,
并且头节点的下一个节点线程不为空,则返回true,表示需要放弃锁竞争,让给后面的写锁去竞争锁。
这里其实就是读锁避让一下写锁,让写锁不至于饥饿。
	final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
	 }

公平锁读锁

	final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
 	}
 
	public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        // 如果头节点不等于尾节点(说明是有等待锁的节点),并且头节点的下一个节点为空或者
        头节点的下一个节点线程不等于当前线程,则返回true,放弃读锁竞争。
        这时说明队列中有节点在等待锁,所以为了公平,先进先出,当前线程必须加到队列尾部等待
        获取读锁。这里判断h.next == null 是因为当第一个节点插入队列时,
        头节点可能会为空,如下addWaiter方法可以看出
        return h != t &&  ((s = h.next) == null || s.thread != Thread.currentThread());
	 }
	private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 如果是第一个节点入队列,这里tail=head,pred=head
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 这里把新节点设置为tail了,此时head != tail
            if (compareAndSetTail(pred, node)) {
            	// 下面这行代码执行之前,head.next == null
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
	}

当第一次获取锁失败时,不是立刻返回失败,而是再次进入轮询获取锁。fullTryAcquireShared主要作用是防止死锁的发生。

	final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (; ; ) {
            int c = getState();
            // 判断是否有写锁获取了独占锁
            if (exclusiveCount(c) != 0) {
                // 这里说明其他线程获取了独占锁,可以立刻返回获取失败
                if (getExclusiveOwnerThread() != current)
                    return -1;
            } else if (readerShouldBlock()) {
                // 进到这里就是为了避免重入锁阻塞,如果重入锁阻塞了,
                // 那么锁就不放释放,后面的节点就会发生锁饥饿。
                // 这里用readerShouldBlock()方法判断是因为之前的tryAcquireShared方法
                // 直接遇到readerShouldBlock方法为true时是直接进来fullTryAcquireShared方法的。
                if (firstReader == current) {
                    // 如果第一个获取读锁的是当前线程,那么直接跑下面代码去竞争锁
                    // assert firstReaderHoldCount > 0;
                } else {
                    // 这个分支就是判断是否为重入的读锁,如果当前线程获取的读锁数为0,
                    // 那么获取锁失败,否则是重入锁,需要参与锁竞争。
                    if (rh == null) {
                        rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current)) {
                            rh = readHolds.get();
                            if (rh.count == 0)
                                readHolds.remove();
                        }
                    }
                    if (rh.count == 0)
                        return -1;
                }
            }
            if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            if (compareAndSetState(c, c + SHARED_UNIT)) {
                if (sharedCount(c) == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    if (rh == null)
                        rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                    cachedHoldCounter = rh; // cache for release
                }
                return 1;
            }
        }
    }

下面讲解第一阶段获取锁失败时,节点会被加入队列,自旋获取锁

	// 这个方法主要是自旋获取锁,如果获取不了则进入阻塞队列等待前面节点唤醒
 	private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                	// 如果前一个节点是头节点,则可以参与锁竞争获取锁。
                	// tryAcquireShared方法上面有讲解过
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    	// 设置当前节点为头节点,并且唤醒后面的连续的读锁,
                    	// 即唤醒后面的读锁参与锁竞争。
                    	// 如果下一个读锁又获得锁还会唤醒后面读锁参与竞争
                    	setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 判断是否立刻阻塞,有可能还不需要阻塞,可以继续参与锁竞争。
                // 后面解读shouldParkAfterFailedAcquire时会讲到
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
	}

下面方法是唤醒后面的读锁

	private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       // 这里两次判断了h是否为空,两次都是为了检验头节点是否还是为空(有可能第一次是空,
       // 第二次不为空了,因为这里是不加锁的判断,头节点随时会变化。
       // 如果第二次不为空就判断他的等待状态是否小于0.之所以h为null还可以唤醒后面节点,
       // 是因为判断为null的时候,可能有节点正在入队列,为了不错过唤醒读锁,
       // 所以会让它进入doReleaseShared方法去唤醒后面的读锁)
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
            	// 后面讲解这方法
                doReleaseShared();
        }
 	}

唤醒读锁的方法

	private void doReleaseShared() {
      
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 如果头节点等待状态为signal(表示后面节点需要阻塞),
                // 则把头节点等待状态改为0,表示后面节点无须等待
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒后继节点,后面讲解
                    unparkSuccessor(h);
                }
                // 如果头节点已经是0,则直接改为PROPAGATE状态,
                // 则后面节点竞争锁失败时不会马上阻塞,会再次竞争锁
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果头节点没有改变说明唤醒完毕,退出轮询。
            // 如果头节点改变了,说明其他线程获取到了锁,为了让唤醒传递下去,继续轮询
            // 这里思考好久,会不会一直不相等呢,其实如果要一直不相等,就得后面唤醒的线程
            // 获得锁并更改了head,但是后面线程涉及到线程调度,不可能一直比这里执行得快
            // 所以看代码是可能会一直不相等,但是实际运行起来是不可能的
            if (h == head)                   // loop if head changed
                break;
        }
    }

唤醒后继节点

	private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
        	// 等待状态设置为0,表示后继节点参与锁竞争时无须入队列等待锁
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        // 如果后继节点为空,或者被取消,
        // 则需要从尾节点轮询找到下一个可以被唤醒的有效节点
        // (为什么要从后面轮询,是因为入队列时,h.next有可能为null)
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

当竞争锁失败时判读是否阻塞

	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
           // 如果前驱节点是signal,则表示需要阻塞当前线程
            return true;
        if (ws > 0) {
           // 如果前驱节点被取消了,则向前找到未被取消的节点,
           // 把当前节点加到后面
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果等待状态是0,或者是传播(PROPAGATE)状态,
            // 则把前驱节点设置为sigal。当前节点可以再次参与锁竞争一次,
            // 如果还是失败,那么就会直接进入队列等待锁。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

2.读锁解锁过程

如果理解了以上加锁过程,那么解锁就很容易理解了,直接看代码吧。

	// 这里不解释sync了,其实它就是AQS的子类
	public void unlock() {
            sync.releaseShared(1);
	}

	public final boolean releaseShared(int arg) {
		// 尝试释放共享锁,如果返回true,说明需要唤醒后继节点,否则直接返回
        if (tryReleaseShared(arg)) {
        	// 唤醒后继节点,这个方法在上面解锁过程解析过,不再讲解
            doReleaseShared();
            return true;
        }
        return false;
 	}
	 // 尝试解锁过程
	protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            // 当前线程是第一个获取锁的线程
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
            	// 这个分支没啥讲得,就是把当前线程加锁次数减一
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            // 这里把读锁数减一
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // 只有在没有读锁和写锁的情况下才需要唤醒后继节点,
                    // 因为如果队列有读锁的话,读锁加锁过程会唤醒后面读锁,
                    // 如果队列有写锁,那么读写、写写都是是互斥的,不需要唤醒。
                    return nextc == 0;
            }
        }

四、写锁

1. 写锁加锁过程

	public void lock() {
            sync.acquire(1);
	 }
 
	public final void acquire(int arg) {
		// acquireQueued方法跟doAcquireShared类似就不讲解了,直接看源码就好
        if (!tryAcquire(arg)  &&  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
	}

	// 尝试获取写锁
	protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            // c不等于0表示有锁
            if (c != 0) {
                // w等于0表示没有写锁,或者有写锁但不是当前线程持有,则返回获取锁失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            // writerShouldBlock方法的公平锁实现是判断队列是否有节点,
            // 非公平锁直接是false,不需要避让
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

2. 写锁解锁过程

	public void unlock() {
           sync.release(1);
	}
	public final boolean release(int arg) {
		// 尝试解锁
        if (tryRelease(arg)) {
            Node h = head;
            // 唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
	 }
 
	protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            // 判断独占锁的数量,因为写锁也是可以重入的,所以不一定写锁加锁数是0
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            // 因为是独占锁,所以这里可以不加锁直接改变加锁的数量
            setState(nextc);
            return free;
	}

五、总结

读写锁花不少时间去看源码,里面有些逻辑不是看看就可以理解的,必须要多问问自己为什么是这样写。只能惊叹作者为了不让机器在同步锁上消耗性能,直接折腾自己的脑袋了。其实我也是佩服AQS算法的精妙,既能保护共享资源,又能保证效率。

文章解析部分是基于自己的理解,有不正确的地方欢迎指正。

参考:并发编程——详解 AQS CLH 锁

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值