并发编程系列(九)—深入理解基于AQS的ReentrantLock

并发编程系列(九)—深入理解基于AQS的ReentrantLock_第1张图片

前言

大家好,牧码心今天给大家推荐一篇并发编程系列(八)—深入理解基于AQS的ReentrantLock的文章,希望对你有所帮助。内容如下:

  • ReentrantLock概要
  • ReentrantLock 数据结构
  • ReentrantLock 使用方式
  • ReentrantLock 实现原理

ReentrantLock概要

ReentrantLock是一种可重入的独占锁,实现了Lock接口,依赖于AQS实现的同步锁机制,具有synchronized基本相同的行为,但也扩展了更多功能,如可中断,非公平和公平锁等,为了帮助大家更好地理解ReentrantLock的特性,我们先将ReentrantLock跟Synchronized进行比较:

维度 synchronized ReentrantLock
锁的类型 可重入,非公平 可重入,非公平,公平
锁的状态 不可中断 可中断
锁的释放 JVM自动释放 通过unlock()显示释放
锁的获取 JVM隐式获取 Lock()获取锁
锁的实现机制 通过JVM监视器实现 通过AQS实现

从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized 同步锁由于具有分级锁的优势,性能上与 Lock 锁差不多;但在高负载、高并发的情况下,Synchronized 同步锁由于竞争激烈会升级到重量级锁,性能则没有 Lock 锁稳定。

ReentrantLock 数据结构

ReentrantLock 实现了Lock接口,通过构造器提供了指定公平策略 / 非公平策略的功能,默认为非公平策略,主要函数说明如下:

  • 类结构图
    并发编程系列(九)—深入理解基于AQS的ReentrantLock_第2张图片
    说明如下:

    • NonfairSync:ReentrantLock的内部类,继承自Sync,非公平锁的实现类;
    • FairSync:ReentrantLock的内部类,继承自Sync,公平锁的实现类;
    • Sync:ReentrantLock的内部抽象类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现;
    • ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。
  • 主要函数

构造函数
// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)
获取锁,判断锁,释放锁等函数
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()

ReentrantLock 使用方式

ReentrantLock 典型使用方式如下:

public void test () throw Exception {
	// 1.初始化选择公平锁、非公平锁
	ReentrantLock lock = new ReentrantLock(true);
	// 2.可用于代码块
	lock.lock();
	try {
		try {
			// 3.支持多种加锁方式,比较灵活; 具有可重入特性
			if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
		} finally {
			// 4.手动释放锁
			lock.unlock()
		}
	} finally {
		lock.unlock();
	}
}

ReentrantLock 实现原理

我们从ReentrantLock的类结构中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。如图所示:
并发编程系列(九)—深入理解基于AQS的ReentrantLock_第3张图片
从图中我们可以看到ReentrantLock支持公平锁和非公平锁,并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢?下面我们分别根据这2中锁的加锁和释放锁流程来分析实现原理。

  • AQS的核心思想参考并发编程系列(八)—初识JUC锁和AQS

ReentrantLock的公平锁加锁原理

我们先假设一个场景,如下:

假设现在有几个线程T1,T2,同时去竞争同一个共享资源,在公平锁机制下又是一个怎样的流程呢?我们从线程T1获取锁开始,ReentrantLock lock = new ReentrantLock(true)

  • 线程T1获取锁
    线程T1会调用lock方法,获取独占锁,该方法内部实现如下:
public void lock() {
        sync.lock();
    }

其实是调用了FairSync的lock方法:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        final void lock() {
            acquire(1);
        }

而FairSync的lock方法有调用了acquire(1); acquire()方法是AQS中定义的方法:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我们可以看到核心实现是来自tryAcquire和acquireQueued这2个方法。

  • tryAcquire()方法
    tryAcquire()的作用是让当前线程获取独占锁,获取成功则返回true,否则返回false;在ReentrantLock的实现逻辑如下:
protected final boolean tryAcquire(int acquires) {
			//1.得到当前线程
            final Thread current = Thread.currentThread();
            // 2.获取独占锁的同步状态state
            int c = getState();
            // 3.若state的值为0,表示锁未占用,则
            if (c == 0) {
            	// 3.1 若等待队列CLH中,当前线程前有没有等待更久的线程,若没有则以CAS更新同步状态为1
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 3.2 更新成功,设置当前线程拥有独占锁
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 4判断是否属于重入情况,重入时同步状态累加+1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                // 4.1超出次数,则溢出
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 4.2设置同步状态
                setState(nextc);
                return true;
            }
            return false;
        }

线程T1获取锁成功后,接着线程T2来竞争锁,此时调用tryAcquire()方法获取锁失败,则会调用addWaiter()方法。

  • addWaiter()方法
    addWaiter()方法作用是:将当前线程包装成一个独占锁结点,并将该节点添加到CLH等待队列尾部,等待获取锁。在AQS中实现的逻辑如下:
private Node addWaiter(Node mode) {
		// 1.创建当前线程对应的Node节点,并设置锁的模型是mode
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 2.若CLH队列不为空,则将当前线程的节点添加到队尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 3.如果CLH队列为空,则新建一个CLH表头;然后将node节点添加到CLH末尾。否则,直接将node节点添加到CLH末尾。
        enq(node);
        return node;
    }

线程T2被包装成等待获取锁的节点插入到CLH队尾后,接着在调用acquireQueued()方法。

  • acquireQueued() 方法
    acquireQueued()的作用是自旋的去执行CLH队列的线程,如果当前线程获取到了锁,则返回;否则当前线程进行休眠,直到唤醒并重新获取锁了才返回。在AQS中的实现逻辑如下:
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        	// 1.interrupted标识在CLH队列的调度中,当前线程进入阻塞时,有没有被中断过。
            boolean interrupted = false;
            // 2.从等待队列CLH的选取队首节点的线程,并尝试获取锁,若获取不到,则会进入阻塞状态
            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) &&
                	// 阻塞当前线程,并且返回线程被唤醒之后的中断状态。通过LockSupport.park()阻塞“当前线程”,然后通过Thread.interrupted()返回线程的中断状态
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • shouldParkAfterFailedAcquire()方法
    shouldParkAfterFailedAcquire()方法作用是判断是否需要阻塞当前线程,在AQS中实现逻辑如下:
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 		// 1.得到前驱节点状态
        int ws = pred.waitStatus;
        // 2.如果前驱节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true。
        if (ws == Node.SIGNAL)
            return true;
        // 3.若前驱节点是取消状态,则设置当前线程的节点为前驱节点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
           // 4.若前驱节点是0或共享锁的状态,则需要设置前驱节点为SIGNAL状态。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

关于节点状态说明:

节点状态 说明
SIGNAL -1 发信号,表示当前线程的后继线程需要被unpark(唤醒)。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程
CANCELLED 1 取消。表示后驱结点被中断或超时,需要移出队列
CONDITION -2 条件,表示当前线程在等待Condition唤醒
PROPAGATE -3 共享,表示其它线程获取到共享锁

至此公平锁加锁的流程分析结束,现在总结下整个加锁过程:
1.线程T1调用acquire()方法获取锁,会先通过tryAcquire()尝试获取锁。获取成功的话,直接返回;尝试失败的话,再通过acquireQueued()获取锁;
2.若第一步线程T1获取成功,由于公平性原则,线程T2则会获取失败,则会先通过addWaiter()来将当前线程加入到CLH队列末尾;然后调用acquireQueued(),在CLH队列中排序等待获取锁,在此过程中,线程T2处于休眠状态,直到获取锁了才返回;
3.如果线程T2在休眠等待过程中被中断过,则调用selfInterrupt()来自己产生一个中断。

ReentrantLock的公平锁释放锁原理

依据公平性原则,线程T1用完后将调用unlock()方法来释放锁,实现逻辑如下:

  • unlock方法
public void unlock() {
        sync.release(1);
    }

可以看到unlock()内部调用的是AQS的release方法,传参为1。

注意:“1”的含义和“获取锁的函数acquire(1)的含义”一样,它是设置“释放锁的状态”的参数。由于“公平锁”是可重入的,所以对于同一个线程,每释放锁一次,锁的状态-1。

  • release() 方法
    release() 方法作用是先调用tryRelease()来尝试释放当前线程锁持有的锁。成功的话,则唤醒后继等待线程,并返回true。否则,直接返回false。在AQS中实现逻辑如下:
     protected final boolean tryRelease(int releases) {
     		// 1.同步状态值-1
            int c = getState() - releases;
            //2.判断持有锁线程和释放锁线程是不是同一个,否则抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 若C=0 说明没有线程占用锁,并清除占用线程
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 更新状态值
            setState(c);
            return free;
        }

线程T1释放成功后,调用unparkSuccessor方法,唤醒队列中的首结点。

  • unparkSuccessor 方法
    其作用 唤醒当前线程的后继线程,后继线程被唤醒之后,就可以获取锁并恢复运行了。具体实现逻辑如下:
private void unparkSuccessor(Node node) {
		// 1.获取当前线程的状态
        int ws = node.waitStatus;
        // 2.如果状态<0,则通过CAS设置状态=0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        // 获取当前节点的有效的后继节点,无效的话,则通过for循环继续获取。当后继节点对应的线程状态<=0为有效。
        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);
    }

队首节点线程T2被唤醒后,获得独占锁资源,继续执行完并也调用unlock方法释放锁。

ReentrantLock的非公平策锁原理

ReenrantLock非公平锁的内部实现中释放锁和公平锁的实现一样,不同的地方是在获取锁。非公平锁和公平锁在获取锁的主要区别在于:
1.公平锁会判断CLH等待队列中是否有线程排在当前线程前面。只有没有情况下,才去获取锁。
2.非公平锁是只要当前锁处于空闲状态,则直接获取锁,而不管在CLH等待队列中的顺序。只有当非公平锁尝试获取锁失败的时候,它才会像公平锁一样,进入CLH等待队列排序等待。实现逻辑如下:

final void lock() {
		// 1.先尝试修改同步状态,若获取失败后再调用AQS的acquire方法
      if (compareAndSetState(0, 1))
          setExclusiveOwnerThread(Thread.currentThread());
      else
          acquire(1);
  }

acquire方法会调用非公平锁自身的tryAcquire方法,最终是调了nofairTryAcquire方法,而该方法相对于公平锁,只是少了“队列中是否有其它线程排在当前线程前”这一判断:
并发编程系列(九)—深入理解基于AQS的ReentrantLock_第4张图片
最后用一个逻辑流程图来总结ReetrantLock 获取锁的过程,如图所示:
并发编程系列(九)—深入理解基于AQS的ReentrantLock_第5张图片

你可能感兴趣的:(并发编程)