ReentrantLock源码解读(condition原理)

回顾

ReentrantLock源码解读(lock、unlock原理)
前面我们看了ReentrantLock的加锁解锁原理,明白了ReentrantLock通过AbstractQueuedSynchronizer实现了保护临界区的功能。让一个线程尝试去获取锁的时候,如果当前锁已经被其他线程占用,那么该线程就会阻塞,加入到阻塞队列中。直到持有锁的线程调用unlock,释放锁之后才会从阻塞队列中唤醒一个线程进入临界区

阻塞队列不止一个

上面我们提到,如果锁被其他线程占用,尝试加锁的线程将会被加入到阻塞队列中,来实现只允许一个线程进入临界区。事实上,在ReentrantLock中,阻塞队列不止一个,可以有n个。说到这里,引入我们今天要讲的,Condition。Condition的引入,使得ReentrantLock可以实现非常多的现实场景问题。

  • 举一个过闸机的例子,闸机就像一把锁,每次只允许一个人(线程)经过,想要过闸机的人排成队(阻塞队列)。
    此时小明发现自己没带身份证,无法过闸机(被阻塞了),可此时小明已经进入闸机了(已经占有锁了),其他人都不能进去了。那该怎么办呢?总不能所有人都过不去吧。
    好,现在工作人员来了:小明不要着急!你是身份证没带是吧,你先从原来的队伍离开(释放原来的锁),排到另一个队,让家人先把身份证送过来,你再过闸机。
    这样,就不会影响其他人过闸机了(获得锁),而当小明拿到身份证之后,又可以重新过闸机。

上面就提到的利用另外一个阻塞队列,解决了过闸机的问题,这里的condition同理。
再看一个condition解决经典的生产者消费者模式的代码:

package juc;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockConditionTest {
	
    /**
     * 管程(包含了共享变量、操作过程、数据结构)
     */
    static class MyMonitor {

        private static final int MAX = 5;
        // 定义重入锁
        private ReentrantLock rl = new ReentrantLock();
        // 共享区 未满条件
        private Condition notFull = rl.newCondition();
        // 共享区 非空条件
        private Condition notEmpty = rl.newCondition();

        /**
         * 缓冲区
         * 缓冲区用什么数据结构,根据实际需求选择
         */
        private Queue<String> container = new ArrayBlockingQueue<>(50);

        /**
         * 生产方法
         */
        public void produce() {
            rl.lock();
            try {
                // 当共享区大小超过最大值,就不能再生产。未满条件就要阻塞
                if (getSize() >= MAX)
                    notFull.await();
                doProduce();
                // 当共享区有元素时,非空条件满足,唤醒因非空条件阻塞的线程
                if (getSize() >= 1)
                    notEmpty.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rl.unlock();
            }
        }

        /**
         * 消费方法
         */
        public void consume() {
            rl.lock();
            try {
                // 共享区元素消费完,非空条件就不满足,要阻塞
                if (getSize() == 0)
                    notEmpty.await();
                doConsume();
                // 共享区元素小于MAX最大值,要因未满条件阻塞的线程
                if (getSize() < MAX)
                    notFull.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rl.unlock();
            }
        }
        
        private int getSize() {
            return container.size();
        }

        /**
         * 真实生产方法
         */
        private void doProduce() {
        	// 这里其实可以直接采用blockQueue的put方法实现
            container.offer("product");
            System.out.println("生产者生产product到缓冲区,当前大小:" + container.size() + "  当前时间:" + System.currentTimeMillis());
        }

        /**
         * 真实消费方法方法
         */
        private void doConsume(){
        	// // 这里其实可以直接采用blockQueue的take方法实现
            container.poll();
            System.out.println("消费者从缓冲区消费product,当前大小:" + container.size() + "  当前时间:" + System.currentTimeMillis());
        }
    }


	/**
	 * 主线程,启动生产者和消费者
	 * @param args
	 */
	public static void main(String[] args) {
		MyMonitor myMonitor = new MyMonitor();

		Thread produce = new Thread(() -> {
			while (true) {  // 生产者线程
				myMonitor.produce();
			}
		});
		Thread consume = new Thread(() -> {
			while (true) {  // 消费者线程
				myMonitor.consume();
			}

		});
		consume.start();
		produce.start();
	}
}

这里使用管程实现(管程可以参考通过管程和信号量分别实现生产者消费者模式
我们可以看到,定义了两个condition,notFull和notEmpty。

  • 当共享区满了的时候,生产者线程就不能继续生产了,需要排队,就排在notFull队伍里。当消费者线程消费了一个之后,共享区留出空间,未满条件成立,就从notFull队伍里唤醒一个线程进行生产。
  • 而当共享区为空,消费者线程就不能继续消费,就排在notEmpty队伍里。当生产者生产一个产品之后,共享区有产品了,非空条件成立,则从notEmpty队伍了唤醒一个消费者线程。
上源码

看了上面的讲解,其实我们大概已经清楚。condition的await()方法,是一个排队的过程。(将线程加入到等待队列中。)而singal方法,是从队伍里唤醒一个线程继续执行(将线程从等待队列中取出)。singal与singalAll的区别就是唤醒一个与唤醒全部
先看await:
在AbstractQueuedSynchronizer内部类ConditionObject中对await的实现:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 新增条件等待节点到队尾
            Node node = addConditionWaiter();
            // 释放当前持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果释放锁成功(判断是否还在锁的同步队列里面),则挂起当前线程
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 后续处理
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

上面主要需要关注的是:fullyRelease,调用await之后会释放当前线程持有的锁。

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

可以看到,await方法会将当前线程加入阻塞队列,通过释放当前线程持有的锁。
下面看一下signal与signalAll:

        public final void signal() {
        	// 如果不是当前持有锁的线程,会抛出非法管程状态错误,所有await、signal要在临界区中使用
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 取出第一个等待线程,并唤醒
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        private void doSignal(Node first) {
            do {
            	// 如果只有一个等待线程,把lastWaiter 赋为null。同时firstWaiter 指向下一个
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
                // 只唤醒一个线程
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

我们看一下transferForSignal方法:

    final boolean transferForSignal(Node node) {
		// 尝试移除condition阻塞状态,失败会返回false,尝试唤醒下一个
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		// 移除成功之后,就让该线程去竞争锁,加入竞争锁的阻塞队列中
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果ws是取消状态,或将ws置为signal失败的话,重新挂起线程。此时就放弃signal操作了,认为成功
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

我们可以看到上面这段:

        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;

如果线程重新挂起,我们还是认为signal是成功的,所以某些特殊场景下,可能是有问题的。即使你await和signal的是同一个线程,但很多源码还是将signal改为了signalAll更加稳健
看下signalAll:

        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            // 尝试对所有阻塞线程进行唤醒,去竞争锁
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }
小结:
  • await:将线程加入到对应condition的阻塞队列,等待signal或signal唤醒。其中使用了LockSupprt.park挂起线程,释放线程拥有的锁。
  • signal和signalAll:唤醒condition的阻塞队列的线程重新去竞争锁。区别是唤醒阻塞队列中的一个与全部。
  • 需要注意的是:以上的方法必须在lock与unlock之间使用,否则会抛出IllegalMonitorStateException异常。这个与Object的wait、notify、notifyAll只能在synchronized关键字内使用一样同理。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值