基于ReentrantLock深入分析AQS原理

基于ReentrantLock深入分析AQS原理

此篇文章基于JDK8来分析的,在JDK9及以后的版本源码实现略有不同,不过思路是一样的,只是在JDK9中推出了新的类型 VarHandle 变量句柄,替代Unsafe的大部分功能。

Java中大部分同步类(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock等)都是基于AQS实现的。AQS提供了原子式管理同步状态(state)、可以阻塞和唤醒线程、FIFO双端队列模型实现的简单框架。AQS内部队列是CLH队列的变种。本篇文章从ReentrantLock入手,分析加锁和解锁过程,在ReentrantLock的场景下,AQS的使用及源码剖析。本文只是理解同步工具类及AQS原理的入门,基于这些原理,自己可以更深入的研究其他同步类中AQS的应用。

1 ReentrantLock

1.1 ReentrantLock特性

ReentrantLock和Synchronized锁

两者的共同点:

  1. 都是可重入锁,同一个线程可以多次获得同一把锁。
  2. 都保证了可见性和互斥性。
  3. 都是用来协调多线程对共享对象、变量的访问控制。

两者的不同点:

  1. ReentrantLock需要手动的加锁和解锁,synchronized系统自动释放锁。
  2. ReentrantLock支持公平锁,synchronized不支持。
  3. ReentrantLock可以尝试获取锁,设置获取锁超时时间,获取锁过程中可响应中断,使用起来更灵活。synchronized等待获取锁的过程中不能被中断。
  4. ReentrantLock是API级别的,synchronized是JVM级别的。
  5. Lock是接口,synchronized是关键字。
  6. ReentrantLock可以绑定多个Condition队列,synchronized只能绑定一个。
  7. 底层实现不同,synchronized是同步阻塞,使用的是悲观并发策略。ReentrantLock是同步非阻塞,采用的是乐观并发策略。
  8. synchronized发生异常时系统可以自动释放锁,所以不会放生死锁。Lock需要手动的释放锁,如果没有主动释放锁会发生死锁现象。所以Lock需要在finally代码块中释放锁。
  9. Lock使用读锁可以提高多线程读操作的效率。
1.2 分析ReentrantLock与AQS关系

ReentrantLock是使用AQS的同步状态、阻塞队列来实现加锁和解锁操作的,ReentrantLock只是AQS实现的其中一个场景,JUC中好多个并发类都是基于AQS来实现的,不同的场景下AQS的同步状态值代表不同的含义,具体要看同步类是怎么实现的了。

ReentrantLock与AQS关系的其实很简单,AQS相当于一个制定标准的组织,其他任何人和机构想要使用我的功能,必须实现我制定的标准。AQS制定的标准就是,所以子类继承我AQS之后,都需要实现我定义的方法,如tryAcquire 和 tryRelease 等(他里面有很多方法,具体实现哪个子类根据自己的功能来定)。这些方法不是通过子类的对象直接来调用的,是AQS自己内部封装好自己调用的,结合阻塞队列一起使用的。所以ReentrantLock类的lock方法是调用AQS的acquire,AQS的acquire方法调用tryAcquire和加入阻塞队列等方法,加入队列的方法是AQS的核心功能,所以不用继承的子类自己实现。以ReentrantLock的非公平锁为例,看下图就能明白加锁和解锁方法调用流程了:

acquire是一个模板方法,内部调用了 tryAcquire 、addWaiter、acquireQueued 方法,其中 tryAcquire 方法是在子类中重写的,所以 tryAcquire 就是一个钩子方法,具体实现子类来决定。

下面是ReentrantLock非公平锁类的关系图:

NonfairSync是ReentrantLock的内部类,NonfairSync继承自Sync,Sync继承自AbstractQueuedSynchronizer。通过这张类的关系图,再结合上一张方法之间调用图,应该就清楚加锁和解锁的大体流程了。

到这里,ReentrantLock和AQS的关系大概介绍完了,下面要详细的介绍AQS的原理,深入分析源码。

2 AQS原理分析

2.1 AQS原理概述

从整体到细节,按照以下六步流程来剖析AQS框架:

2.1.1 AQS原理概述
2.1.2 AQS数据结构

在AQS源码之前,需要先了解AQS的底层数据结构,只要掌握了它的数据结构,才能理解源码的实现。甚至需要度很多遍源码,反复的思考,才能理解作者这么写的意图。

AQS的内部数据结构——Node,Node是双向链表,是CLH队列的变体,下面介绍一下Node节点的属性和方法:

方法属性解释
waitStatus当前节点在队列中的状态
prev前驱节点
next后继节点
thread当前节点的线程
nextWaiter指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍)
predecessor()返回前驱节点,若没有抛出NullPointerException

获取锁的两种模式:

模式含义
SHARED表示多个线程可以共享同一把锁
EXCLUSIVE表示线程以独占的方式获取锁

waitStatus 几个枚举值的含义:

枚举值含义
0节点被初始化时的默认值
SIGNAL-1,当前线程释放或取消锁需要唤醒后继节点
CANCELLED1,当前线程以取消或中断状态
CONDITION-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
2.1.3 AQS同步状态

在了解数据结构后,接下来了解一下AQS的同步状态——State。AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;

state这个变量是AQS的核心,具体的含义需要继承自AQS的同步子类自己来决定。操作state的方法如下:

方法含义
protected final int getState()获取state值
protected final void setState(int newState)设置state值
protected final boolean compareAndSetState(int expect, int update)通过CAS方式更新state值

这几个方法都是Final修饰的,说明子类中无法重写它们。

2.1.4 AQS加入队列

通过分析ReentrantLock的加锁过程入手,里分析AQS入队列的源码。通过下面源码分析来了解。

2.1.5 AQS移除队列

通过分析ReentrantLock的解锁过程入手,里分析AQS出队列的源码。通过下面源码分析来了解。

2.1.6 AQS中断机制

AQS中用到了协作式中断的知识,AQS不处理中断,把中断结果返回给同步器自己实现,具体源码体现在 acquireQueued 方法的 parkAndCheckInterrupt 方法中,Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),中断后线程也或继续获取锁,只是记录中断结果。

2.2 从ReentrantLock到AQS源码分析

下面分析ReentrantLock非公平锁加锁流程:

分析源码必须要跑起来,然后打断点一步一步执行。通过加锁和解锁入手分析

public class Lock_01 {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
        lock.lock();
        lock.unlock();
    }
}
// java.util.concurrent.locks.ReentrantLock
public void lock() {
    sync.lock();// 调用NonfairSync的lock方法
}
// java.util.concurrent.locks.ReentrantLock#NonfairSync
final void lock() {
    if (compareAndSetState(0, 1))// CAS设置AQS的state状态值为1
        setExclusiveOwnerThread(Thread.currentThread());// state设置成功,保存获取锁的线程
    else
        acquire(1);// 调用AQS的acquire方法
}

由于是非公平锁加锁,所以直接通过CAS修改state值,若抢到锁了,就不用进入等待队列中。这就是为什么推荐使用非公平锁,因为效率高。那为什么效率高呢?

答:进入队列需要阻塞和唤起线程,涉及到线程的上线文切换,导致系统上下文切换。在这过程中所消耗的时间大于持有锁的时间,所以线程进入队列浪费资源。另外一点,通过CAS的方式把节点加入对尾,当并发量比较大,会一直在自旋,浪费CPU资源。

上来直接 compareAndSetState(0, 1) ,设计的非常巧妙,虽然损失了代码整洁度,但是提高了性能。即使没有if这段代码,直接调用 acquire 方法也不会有问题,acquire 方法中有重复的代码。下面 addWaiter 方法中还有类似的设计。

// java.util.concurrent.locks.AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 尝试获取锁,tryAcquire方法调用的是子类的方法,调用NonfairSync.tryAcquire(arg),这个方法就相当于钩子方法,父类定义方法让子类自己实现
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// AQS中tryAcquire方法实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
  • tryAcquire方法:调用NonfairSync的tryAcquire方法,尝试获取锁,锁获取成功后,不在执行后续的加入队列的方法。
  • addWaiter方法:把当前线程以安全的方式加入队列尾部。
  • acquireQueued方法,尝试获取锁,获取失败阻塞当前线程。

以上这个三个方法 tryAcquire、addWaiter、acquireQueued 是AQS核心方法。tryAcquire 获取锁的核心方法,具体实现由同步器自己实现,所以自定义同步锁,加锁只需要实现tryAcquire方法就可以了。addWaiter 和 acquireQueued 是AQS的核心方法,对外透明,自定义同步锁可以不用关心这两个方法。

下面针对这三个方法(tryAcquire、addWaiter、acquireQueued)进行详细分析。

分析tryAcquire方法,AQS中acquire方法中调用的tryAcquire方法是NonfairSync的tryAcquire:

// java.util.concurrent.locks.ReentrantLock#Sync#NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);// 调用Sync类的nonfairTryAcquire方法
}
// java.util.concurrent.locks.ReentrantLock#Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();// 获取当前线程对象
    int c = getState();// 获取AQS中state状态值
    if (c == 0) {// c==0,说明当前没有线程获取锁
        if (compareAndSetState(0, acquires)) {// 通过CAS尝试修改state值
            setExclusiveOwnerThread(current);// state修改成功,保存当前线程为独占锁线程
            return true;// 返回true,获取锁成功
        }
    }
    else if (current == getExclusiveOwnerThread()) {// 判断是否重入锁,当前线程是否是独占锁线程
        int nextc = c + acquires; // 重入锁,获取锁次数加1
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");// 锁重入次数超过了int的范围,应为state的类型时int
        setState(nextc);// 设置state值,保存加锁次数
        return true;// 返回true,获取锁成功
    }
    return false;// 返回false,获取锁失败
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected final void setState(int newState) {
    state = newState;
}

tryAcquire 方法分析完了,接着分析 addWaiter 方法。tryAcquire 获取锁失败把当前线程创建Node节点,加入AQS队列中。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) && // tryAcquire 返回false,执行后续方法
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

下面分析 addWaiter 方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);// 使用当前线程创建队列节点,node为独占模式
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;// pred 执行当前队尾
    if (pred != null) { // 队列不为空
        node.prev = pred; // 当前节点的前驱节点指向tail
        if (compareAndSetTail(pred, node)) {// 通过CAS把当前节点设置为tail
            pred.next = node;// tail的后继节点指向当前节点
            return node;// 当前节点加入队尾成功,返回当前节点
        }
    }
    enq(node);// 节点加入队列
    return node;// 加入成功,返回当前节点
}

这个方法的设计与上面的lock方法思想一样,损失代码整洁度,提高性能。if代码可以没有,也没有问题,enq中会有重复的代码。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node enq(final Node node) {
    for (;;) {// 死循环,通过自旋的方式,直道成功的把节点加入队尾
        Node t = tail;// t保存当前队尾指针
        if (t == null) { // Must initialize 队列为空
            if (compareAndSetHead(new Node())) // 通过CAS初始化head
                tail = head;// tail指向head节点
        } else {
            node.prev = t;// 当前节点的next指向尾节点
            if (compareAndSetTail(t, node)) {// 通过CAS把当前节点设置为尾结点
                t.next = node;// 原尾结点的next指向当前节点
                return t; 返回原尾结点
            }
        }
    }
}

当t==null时,创建了一个空的节点作为头结点,头结点是空节点不保存任何线程,因为唤醒节点线程继续执行需要头节点。

到此,把当前线程成功加入队列中。接着分析 acquireQueued 方法:

// 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();// 获取node节点的前驱节点
            if (p == head && tryAcquire(arg)) {// 如果当前节点是头结点的后继节点,可以尝试获取锁,应为有可能头结点已经释放锁,但是还没来得及通知后继节点,所以可以尝试去获取锁
                setHead(node);// 获取锁成功,设置当前节点为头节点
                p.next = null; // help GC
                failed = false;
                return interrupted;// 返回线程中断状态
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 找当前节点之前的有效(未取消)节点,当前节点next指向该节点,设置前驱节点的waitState=-1,用于唤醒当前接口
                parkAndCheckInterrupt())// 阻塞线程,返回中断状态
                interrupted = true;// 线程在阻塞的过程中中断过
        }
    } finally {
        if (failed) // 如果线程抛出异常,没有正常执行到这里,failed的值为true,所以会取消当前节点
            cancelAcquire(node);
    }
}

这个方法主要功能如下:

  1. 当前节点尝试获取锁,获取成功,直接返回。获取失败,执行一下步骤;
  2. 找到当前节点的有效前驱节点,并把新找到的前驱节点的waitState值改为-1;
  3. 阻塞当前线程,等待前驱节点唤醒。
  4. 若以上步骤执行的过程中线程抛出异常(中断等),取消当前节点,若当前节点为头结点,唤醒后继节点。

p==head,为什么需要尝试获取锁呢?

答:这个需要结合unlock一起分析了,这里先说一下结果,unlock释放锁后(就是把state改成0),没有来得及通知后继节点,这时当前节点尝试去获取锁,若获取成功后就不用阻塞线程,提高性能,降低阻塞和唤醒的资源浪费,因为涉及到上下文切换。

分析一下 shouldParkAfterFailedAcquire 方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		/*
		 * 前驱节点的waitStatus=-1,当前节点可以安全的被阻塞
		 */
		return true;
	if (ws > 0) {
		/*
		 * 如果前驱节点被取消,跳过前驱节点,继续向前找
		 */
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;// 找到未取消的前驱节点的next指向当前节点
	} else {
		/*
		 * 通过CAS把当前节点的前驱节点waitStatus值改为-1
		 */
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

这一步只要是找到前驱节点,并把前驱节点waitStatus值改为-1,确保当前节点阻塞后,可被唤醒。

分析 parkAndCheckInterrupt 方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);// 阻塞当前线程
    return Thread.interrupted();// 获取线程阻塞的过程中是否被中断,若中断return返回true,并将中断状态改为false。AQS不处理中断操作,具体如何处理由实现类决定。
}

分析 cancelAcquire 方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;

    // 跳过已经取消的前驱节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

	  // node前驱节点的next指针
	  Node predNext = pred.next;

	  // 修改node节点的waitStatus值为-1,已取消状态
	  node.waitStatus = Node.CANCELLED;

	  // 如果node是尾结点,通过CAS把node前驱节点设置为尾结点
	  if (node == tail && compareAndSetTail(node, pred)) {
		    compareAndSetNext(pred, predNext, null);// 通过CAS把前驱节点的next指向null
	  } else {
		    int ws;
		    if (pred != head &&
			    ((ws = pred.waitStatus) == Node.SIGNAL ||
			     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
			    pred.thread != null) {// 前驱节点不是头结点,并且前驱节点的waitStatus是-1或者可以改成-1,并且前驱节点线程不为空
			        Node next = node.next;
			        if (next != null && next.waitStatus <= 0)
				          compareAndSetNext(pred, predNext, next);// 修改前驱节点的next指针
		    } else {
			      unparkSuccessor(node);// 前驱节点是头结点,或者前驱节点已经取消了,唤醒当前节点的后继节点继续执行(但是不一定获取锁,有可能继续被阻塞)
		    }
		    node.next = node; // help GC
	  }
}

分析 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)
			if (t.waitStatus <= 0)
				s = t;
	}
	if (s != null)
		LockSupport.unpark(s.thread);// 唤醒节点线程
}

思考为什么需要从后往前找,而不是从前往后找?

答:前面已经分析节点加入队列的步骤了,先设置当前节点的前驱节点,node.prev = t,再通过CAS设置队列尾结点tail,最后设置prev.next=node。这三步不是原子操作,在尾结点设置成功之后,node.prev=t 一定成功了,但是 prev.next=node 操作并不一定执行,所以从后往前找是没问题的,但是从前往后找就会导致缺失节点。

到这里,非公平锁加锁流程就分析完了,简单总结一下:尝试获取锁,得到锁,成功返回。未得到锁,加入队列,阻塞线程。

非公平锁解锁流程:

public class Lock_01 {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
        lock.lock();
        lock.unlock();
    }
}
// java.util.concurrent.locks.ReentrantLock
public void unlock() {
    sync.release(1);// 调用父类AQS的release方法
}

分析 release 方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
    if (tryRelease(arg)) {// 尝试释放锁,tryRelease这个方法调用子类的实现
        Node h = head;// h指向当前头结点
        if (h != null && h.waitStatus != 0)// 判断是否有需要唤醒的后继节点
            unparkSuccessor(h);// 唤醒头结点的后继节点
        return true;// 返回true,解锁成功
    }
    return false;// 返回false,解锁失败
}

分析 tryRelease 方法:

// java.util.concurrent.locks.ReentrantLock#Sync
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 计算剩余锁重入次数
    if (Thread.currentThread() != getExclusiveOwnerThread())// 判断当前线程是否是获取锁的线程,若不是直接抛出异常
        throw new IllegalMonitorStateException();
    boolean free = false;// 标识是否释放锁,true-释放 false-不释放
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);// 清除当前持有锁的线程
    }
    setState(c);// 修改加锁次数
    return free;// 返回锁释放状态
}

先判断当前线程是否是独占锁持有线程,若不是直接抛出异常。若是,判断state==0,等于0表示当前线程可以释放锁,清除独占锁线程标识,state值设置为0或者剩余锁重入次数。

上面释放锁的动作不是原子性的,在执行完 setState(0) 方法后,锁已经被释放了,此时再来新的线程可以拿到这把锁,因为之前已经分析非公平锁加锁流程了,上线先通过CAS修改state值,修改成功就拿到锁了。

在最极端的情况下,非公平锁会导致已经在队列中的线程一直获取不到锁,也就是锁饥饿问题。不过这种情况发生的概率比较小,可以忽略。若实际生产中并发量特别大,需要解决这个问题,可使用公平锁,没有线程获取锁时都需要先看队列中是否有有效的节点,只要有,当前节点就必须加入队列等待被唤醒。这就可以保证先入队列优先获取到锁。

下面看一下公平锁的加锁流程,主体思路与非公平锁一样,只是尝试获取锁的方式不同,这个不做详细分析了,主要看 tryAcquire 方法:

public class Lock_01 {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock(true);// 默认是非公平锁,ReentrantLock(true) 公平锁,ReentrantLock(false) 非公平锁
        lock.lock();
        lock.unlock();
    }
}
// java.util.concurrent.locks.ReentrantLock
public void lock() {
    sync.lock();
}
// java.util.concurrent.locks.ReentrantLock#FairSync
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;
}

分析 hasQueuedPredecessors 方法:

public final boolean hasQueuedPredecessors() {
    Node t = tail; // t指向尾节点
    Node h = head; // h指向头节点
    Node s;
    return h != t &&// h!=t,说明队列中有节点
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

这个方法的核心就在于最后一句代码,h!=t,说明队列中有节点。h.next==null,什么情况下会发生这种情况呢?理解这种情况,需要考虑往队列里添加节点的步骤:

  1. node.prev = pred,当前节点的前驱节点指向tail
  2. compareAndSetTail(pred, node),当前节点设置为tail
  3. pred.next = node,前驱节点的next指向node

当1,2执行成功了,3还没执行,此时h.next的值就为null。若头结点的后继节点不是当前线程,就加入队列排队,若后继节点是当前线程,就尝试通过CAS修改state值来获取锁。

3 AQS应用

3.1 AQS在JUC中的应用场景

JUC中很多并发工具底层都是基于AQS实现的,下面介绍几种同步工具AQS的应用场景:

同步工具同步工具与AQS的关联
ReentrantLock使用AQS保存加锁的次数。ReentrantLock加锁成功后记录获得锁线程ID,用于判断锁重入和禁止其他线程解锁。
CountDownLatch使用AQS同步状态计数。每执行一次countDown,AQS同步状态值减1,减到0时,所有阻塞线程被唤醒,可以继续执行。
Semaphore使用AQS同步状态保存信号量当前数量。每执行一次acquire,信号量当前数量减1,当减到0时,线程进入AQS阻塞队列,等待其他线程执行release后,才能唤醒阻塞队列头上的线程。
ReentrantReadWriteLockAQS同步状态state是int类型的,占用4个字节32位,低16位存写锁的数量,高16位存读锁的数量。
3.2 自定义同步工具

实现自定义同步工具很简单,只需要继承AbstractQueuedSynchronizor,重写 tryAcquire 和 tryRelease 方法,这两个方法是加锁和解锁的核心实现方法,再提供两个Api接口 lock 和 unlock 用于加锁和解锁,这两个方法内部直接通过自定义同步工具对象调用AQS的 acquire 和 release 方法。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

通过源码可以看到AQS内部并没有实现tryAcquire方法,这个方法需要自定义同步器自己来实现。AQS中acquire方法调用的是子类的tryAcquire,所以tryAcquire是钩子方法,需要继承AQS的子类实现,acquire方法采用模板方法设计模式。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
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 boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

tryRelease方法与上面的tryAcquire原理相同。

根据上面的分析,实现简易版的自定义同步器 MyLock:

public class MyLock {

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(0);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, arg);
        }
        @Override
        protected boolean tryRelease(int arg) {
            setState(arg);
            return true;
        }
    }
}

测试自己定义同步工具MyLock:

public class MyLockTest {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        MyLock lock = new MyLock();

        Runnable r = () -> {
            try {
                lock.lock();
                for (int i = 0; i < 1000000; i++) {
                    count++;
                }
            } finally {
                lock.unlock();
            }
        };
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

不管运营多少遍,最终输出结果都是:2000000

参考资料:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值