【多线程与并发】ReentrantLock与AQS

一、前言

ReentrantLock(以下简称RL)是JDK5之后推出的互斥锁,实现了java.util.concurrent.locks.Lock接口,功能和synchronized关键字几乎一样,但是写法上有区别,而且提供了几个更加灵活的API:

  1. 等待可中断:tryLock(long timeout, TimeUnit unit)以及lockInterruptibly()能够响应中断
  2. 公平锁:RL构造方法提供了一个布尔选项打开公平锁,不过会使性能急剧下降
  3. 多条件:newCondition()能够使线程等待在不同的条件上,减少不必要的线程苏醒。

RL虽然并未继承AQS(AbstractQueuedSynchronizer),但是其内部类Sync继承了AQS,RL的API实现也基本是由sync提供的。AQS是JUC中比较重要的一个基类,它也是很多实现类的模板类,包括RL在内的许多同步器(Semaphore,CountDownLatch、线程池等)都是复用其中的代码完成同步功能的。AQS的主要数据结构是等待队列(双向链表)以及state计数器。

有个很有意思的点就是:AQS虽然被声明为抽象类但是它却没有一个抽象方法,它只有五个抛出UnsupportedOperationException的方法,这几个方法也正是需要子类覆写的方法。这里之所以没有定义成abstract,是因为独占模式(例如RL)下只用实现tryAcquire-tryRelease,而共享模式(例如Semaphore)下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
RL与AQS关系图

二、如何使用RL

下面还是通过一个经典的生产者消费者的例子来看下RL的API使用方式,与【多线程与并发】synchronized同步锁这篇文章相比,除了锁的方式不同外,队列我采用了链表的形式。
链表队列

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerAndConsumerWithRL {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        ProductQueue queue = new ProductQueue(5);
        for (int i = 0; i < 5; i++) {
            pool.execute(new Producer(3, queue));
        }
        for (int i = 0; i < 3; i++) {
            pool.execute(new Consumer(5, queue));
        }
        pool.shutdown();
    }

    private static class Product {
        private static int count = 0;
        private int id = count++;
        private Product next;

        @Override
        public String toString() {
            return "product(id=" + id + ")";
        }

        public void setNext(Product next) {
            this.next = next;
        }

        public Product getNext() {
            return next;
        }
    }

    private static class ProductQueue {
        private int maxSize;
        private int currentSize;
        private Product head;
        private Product tail;
        private ReentrantLock lock = new ReentrantLock();
        // 生产者等待条件队列
        private Condition producerCondition = lock.newCondition();
        // 消费者等待条件队列
        private Condition consumerCondition = lock.newCondition();

        ProductQueue(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("size must > 0");
            }
            this.maxSize = maxSize;
        }

        public void put(Product product) {
            lock.lock();
            try {
                // 队列满时生产者进入生产者等待队列,并且自动释放锁。这个与Object#wait()相同
                while (currentSize == maxSize) {
                    producerCondition.await();
                }
                // 队列为空
                if (head == null && tail == null) {
                    head = product;
                } else {
                    tail.setNext(product);
                }
                tail = product;
                currentSize++;
                // 生产后通知所有消费等待者,而Object#notifyAll()只能通知所有的线程
                consumerCondition.signalAll();
                System.out.printf("%s has been put%n", product);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
            	// unlock通常放在finally中,确保锁一定会释放
                lock.unlock();
            }
        }

        public Product take() {
            lock.lock();
            try {
                // 队列为空时,消费者进入消费者等待队列,并且释放锁
                while (head == null && tail == null) {
                    consumerCondition.await();
                }
                Product currentHead = head;
                Product nextHead = head.getNext();
                currentHead.setNext(null);
                head = nextHead;
                // 队列只有一个元素时
                if (nextHead == null) {
                    tail = null;
                }
                currentSize--;
                // 消费后通知所有生产等待者
                producerCondition.signalAll();
                System.out.printf("%s has been taken%n", currentHead);
                return currentHead;
            } catch (InterruptedException e) {
                e.printStackTrace();
                throw new RuntimeException("interrupted");
            } finally {
                lock.unlock();
            }
        }
    }

    private static class Producer implements Runnable {
        private static int count;
        private int id = count++;
        private int num;
        private ProductQueue queue;

        Producer(int num, ProductQueue queue) {
            this.num = num;
            this.queue = queue;
            System.out.printf("producer%d produces %d products%n", id, num);
        }

        @Override
        public void run() {
            for (int i = 0; i < num; i++) {
                queue.put(new Product());
            }
        }
    }

    private static class Consumer implements Runnable {
        private static int count;
        private int id = count++;
        private int num;
        private ProductQueue queue;

        Consumer(int num, ProductQueue queue) {
            this.num = num;
            this.queue = queue;
            System.out.printf("consumer%d consumes %d products%n", id, num);
        }

        @Override
        public void run() {
            for (int i = 0; i < num; i++) {
                queue.take();
            }
        }
    }
}

三、AQS源码分析

下面通过RL的加解锁来窥探一下AQS的结构及其关键源码

1. 线程第一次抢到锁的场景(非公平)

RL抢到锁
从活动图中可以看到,RL的lock方法是由NonFairSync#lock()实现的,它主要做了两件事:

  1. 通过CAS的方式将state从0设为1,在RL中AQS的state字段可以当做synchronized的monitor计数器,统计锁的重入次数。
  2. 将AOS中的独占线程字段设为当前线程
	// java.util.concurrent.locks.ReentrantLock#lock
	public void lock() {
	    sync.lock();
	}
	
	// java.util.concurrent.locks.ReentrantLock.Sync#lock
	abstract void lock();
	
	// java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
	final void lock() {		
	    if (compareAndSetState(0, 1))	// 第一次获取锁
	        setExclusiveOwnerThread(Thread.currentThread());
	    else							// 获取锁失败或锁重入
	        acquire(1);					// AQS关键方法
	}
	// java.util.concurrent.locks.AbstractQueuedSynchronizer#compareAndSetState
	protected final boolean compareAndSetState(int expect, int update) {
	    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
	}
	
	// java.util.concurrent.locks.AbstractOwnableSynchronizer#setExclusiveOwnerThread
	protected final void setExclusiveOwnerThread(Thread thread) {
	    exclusiveOwnerThread = thread;
	}

2. 线程锁重入的场景

和场景一相比差别主要在ReentrantLock.NonfairSync#lock这个方法上,在本节场景下走的是else分支中的acquire()方法,该方法最终会调用nonfairTryAcquire()中的锁重入分支。

	// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    // java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    
	// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
	final boolean nonfairTryAcquire(int acquires) {
	    final Thread current = Thread.currentThread();
	    int c = getState();
	    // 第一次获取锁
	    if (c == 0) {
	        if (compareAndSetState(0, acquires)) {
	            setExclusiveOwnerThread(current);
	            return true;
	        }
	    }
	    // 锁重入,state+1
	    else if (current == getExclusiveOwnerThread()) {
	        int nextc = c + acquires;
	        if (nextc < 0) // overflow
	            throw new Error("Maximum lock count exceeded");
	        setState(nextc);
	        return true;
	    }
	    // 未获取到锁
	    return false;
	}

3. 线程未抢到锁的场景

线程未抢到锁

场景二中nonfairTryAcquire()能够通过锁重入分支返回true,但是在本节这个场景下该方法由于未抢到锁而返回了false,所以需要接着执行图中的②和③。由于cas操作都依赖于Unsafe类,出于简化图片的目的我就没有标注出来,有需要可以参考场景一中的图片。

方法②实现的主要功能是将当前线程封装成一个节点然后插入等待队列的队尾。Node作为AQS的内部类用来表示节点,它内部保存了当前线程,并且有如下两种模式,RL毫无疑问使用的是模式2

  1. SHARED = new Node()
  2. EXCLUSIVE = null

以及五种状态 :

扫描二维码关注公众号,回复: 11601559 查看本文章
  1. CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  2. SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  3. CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  4. PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  5. DEFAULT(0):新结点入队时的默认状态。
	//java.util.concurrent.locks.AbstractQueuedSynchronizer
	
	private Node addWaiter(Node mode) {
	    Node node = new Node(Thread.currentThread(), mode);
	    // Try the fast path of enq; backup to full enq on failure
	    Node pred = tail;
	    // 前面已经有等待的线程
	    if (pred != null) {
	        node.prev = pred;
	        if (compareAndSetTail(pred, node)) {
	            pred.next = node;
	            return node;
	        }
	    }
	    // 第一个进入等待的线程
	    enq(node);
	    return node;
	}
	    
	private Node enq(final Node node) {
	    for (;;) {
	        Node t = tail;
	        // 队列未初始化,则创建一个空的头结点
	        if (t == null) { // Must initialize
	            if (compareAndSetHead(new Node()))
	                tail = head;
	        } else {
	            node.prev = t;
	            if (compareAndSetTail(t, node)) {
	                t.next = node;
	                return t;
	            }
	        }
	    }
	}

方法③主要是线程出入队列的管理,当线程进入等待队列后会通过cas的方式将前驱结点的status标记为signal然后再进入等待状态

	//java.util.concurrent.locks.AbstractQueuedSynchronizer
	final boolean acquireQueued(final Node node, int arg) {
	    boolean failed = true;
	    try {
	        boolean interrupted = false;
	        for (;;) {
	            final Node p = node.predecessor();
	            // 前驱结点已经执行完毕,当前线程获取到锁
	            if (p == head && tryAcquire(arg)) {
	                setHead(node);
	                p.next = null; // help GC
	                failed = false;
	                return interrupted;		// 该循环中唯一的出口
	            }
	            // 检查当前线程是否可以进入等待状态
	            if (shouldParkAfterFailedAcquire(p, node) &&
	                parkAndCheckInterrupt())
	                interrupted = true;
	        }
	    } finally {
	        if (failed)
	            cancelAcquire(node);
	    }
	}
	
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	    int ws = pred.waitStatus;
	    // 前驱结点的状态是signal,当前线程可以进入等待状态
	    if (ws == Node.SIGNAL)    
	        return true;
	    if (ws > 0) { //如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边
	        do {
	            node.prev = pred = pred.prev;
	        } while (pred.waitStatus > 0);
	        pred.next = node;
	    } else {  //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下
	        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	    }
	    return false;
	}
	
	private final boolean parkAndCheckInterrupt() {
	    LockSupport.park(this);			// LockSupport最终也是利用unsafe的本地方法实现线程阻塞等待
	    return Thread.interrupted();	// 如果被唤醒,查看自己是不是被中断的。
	}

4. 线程解锁的场景

RL-unlock
从上图中可以看出RL的unlock()方法的实现是由AQS的release()提供的,它主要实现两个功能:

  1. 将state计数器减一。
  2. 如果当前线程已经释放了锁,则唤醒后继节点(如果存在的话)
	//java.util.concurrent.locks.ReentrantLock#unlock
	public void unlock() {
	  sync.release(1);
	}
	
	// java.util.concurrent.locks.AbstractQueuedSynchronizer#release
	public final boolean release(int arg) {
	    if (tryRelease(arg)) {
	        Node h = head;
	        if (h != null && h.waitStatus != 0)
	            unparkSuccessor(h);
	        return true;
	    }
	    return false;
	}
	
	// java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
	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;
	}
	
	// java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
	private void unparkSuccessor(Node node) {
	    // 将节点状态设为初始值
	    int ws = node.waitStatus;
	    if (ws < 0)
	        compareAndSetWaitStatus(node, ws, 0);
	
	    // 后继节点不存在或者已经取消等待,则从队尾逆向遍历队列找到第一个等待节点
	    Node s = node.next;
	    if (s == null || s.waitStatus > 0) {
	        s = null;
	        for (Node t = tail; t != null && t != node; t = t.prev) // 这个for循环用的很巧妙,一般来说可能会先想到使用while break的方式。
	            if (t.waitStatus <= 0)
	                s = t;
	    }
	    // 若存在后继等待节点则唤醒
	    if (s != null)
	        LockSupport.unpark(s.thread);
	}

四、参考

Java并发之AQS详解
从ReentrantLock的实现看AQS的原理及应用
深入理解java虚拟机

猜你喜欢

转载自blog.csdn.net/hch814/article/details/106369711