JUC学习之——ReentranLock

1. ReentranLock简介


ReentranLock是自JDK1.5引入的基于API层面的互斥锁,它与sychronized有着一些异同。

  • 相同点:都是独占式的可重入锁,
  • 不同点:ReentranLock的加锁和解锁过程需要手动去控制,synchronized的加锁和解锁是通过JVM来实现的;ReentranLock可以响应中断而sychronized不可以响应中断;ReentranLock可以创建公平锁而synchronized只能创建非公平锁

2. ReentranLock的使用示例


class Ticket{
    private Integer num = 30;

    Lock lock = new ReentrantLock();
    public void sale(){
        lock.lock();
        try {
            if(num > 0){
                System.out.println(Thread.currentThread().getName() + "卖出第 " + (num--) + "张票,还剩下" + num + "张票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        new Thread(() -> {for(int i = 0;i < 40;i++){ t.sale();}},"A").start();
        new Thread(() -> {for(int i = 0;i < 40;i++){ t.sale();}},"B").start();
        new Thread(() -> {for(int i = 0;i < 40;i++){ t.sale();}},"C").start();
    }
}

上面的示例是一个简单的模拟三个窗口售票的情况,票即为共享资源,可以看到对共享资源的访问的加锁和解锁是通过调用 ReentranLock 类中的 lock()unlock()方法实现的。

3. AQS

AQS即ReetranLock的底层源码类AbstractQueuedSynchronizer的首字母简写,通常被称为队列式同步器。
AQS对资源访问的控制是通过虚拟双向队列CLH实现的,即将请求当前空闲资源的线程设为有效的工作线程,然后对资源进行加锁控制,禁止其他线程再对资源类进行访问;将请求当前已被占用资源的线程加入CLH双向队列中进行等待被唤醒。
AQS中维护着一个state变量,这个变量表示状态,在ReentranLock中表示可重入锁的次数。
AbstractQueuedSynchronizer类继承于AbstractOwnableSynchronizer,该类只有一个变量,就是exclusiveOwnerThread,表示当前正在占用锁的进程

4. ReentranLock加锁解锁的实现原理


4.1 前提

想要看懂ReentranLock类的加锁和解锁的实现原理,那么就必须了解一下 CAS 是什么操作。
CAS即CompareAndSwap操作,也就是比较交换的操作,通常情况下,这个步骤是需要多步完成的,肯定不具有原子性,但是在CAS中,该操作可以具有原子性,CAS 包含有三个操作参数:内存地址(V)、期望原值(A)、新值(B)。若内存位置V上存放的值与期望原值A相同,那么将期望原址A更换为新值B,否则不进行任何操作。这个操作有效的实现了假如当前某个位置存放的值是我认为的那个值,就进行更新;否则不进行任何操作的功能,只是需要知道这个值就可以了。

4.2 构造器

首先来看ReentranLock类的构造器

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    }

源码中一共有两个构造器,可以明显看出默认情况下创建的是非公平锁,但是可以通过传参创建公平锁。
再通过观察发现 FairSync 和 NonfairSync 都是 ReentranLock 类的内部类同时继承于其内部类Sync ,并且这两个类中的 lock()unlock()方法其实就是调用了 Sync 类中的方法,Sync 类又继承了 AbstractQueuedSychronizer 类,AbstractQueuedSyschronizer 类又继承于 AbstractOwnableSynchronizer 类。详情见下图:
JUC学习之——ReentranLock_第1张图片

4.2 lock() 加锁

在此只分析 NonfairSync 类
lock.lock()处,调用的是 ReentranLock 类中的 lock()方法,该 lock 方法又调用 Sync 类中的方法,因 Sync 类是一个抽象类,根据多态性,调用的是 NonfairSync 类中重写的 lock()方法。lock()源码如下:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

首先就是一个 CAS 操作,判断 state 是否为 0 ,若为 0 ,说明当前锁未被占用,将其置为1,并且调用 setExclusiveOwnerThread(Thread thread)方法将当前线程设为独占线程,这里的CAS操作足以保证设置state的线程只有一个,其他线程再竞争失败后只能去排队等待。例子中三个线程A、B、C,假设当前线程A的CAS操作成功,那么B和C线程只能进入else分支,执行acquire(int arg)方法。acquire(int arg)方法源码如下:

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

可以清楚得知B和C线程尝试获取锁失败并且被加入到CLH队列中后自身进行阻塞。
接下来分步解析
第一步 尝试获取锁
源码如下

final boolean nonfairTryAcquire(int acquires) {
	//获取当前执行尝试获取锁操作的线程
    final Thread current = Thread.currentThread();
    //获取当前的state值
    int c = getState();
    //若当前没有线程占用锁
    if (c == 0) {
    	//尝试占用锁
        if (compareAndSetState(0, acquires)) {
        	//将当前线程设置为独占线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //若当前线程已占用锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //更新state变量值,即更新可重入锁次数
        setState(nextc);
        return true;
    }
    //执行到此说明当前线程既无法占用锁也没有正在占用锁,故返回false
    return false;
}

整个尝试占用锁的流程总结来说就是:判断当前state值是否为0,若为0,说明锁已经被释放,当前线程可以进行占用;若不为0,那么先检查锁是否是被自己占用,若是,则更新state变量值。以上两点都不成功就说明当前线程无法占用锁也没有正在占用锁,返回false。
第二步 入队

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

先进行addWaiter(Node mode)方法的分析,源码如下:

private Node addWaiter(Node mode) {
	//首先创建结点,将当前线程和模式存储进去
    Node node = new Node(Thread.currentThread(), mode);
    //首先获取尾节点的引用
    Node pred = tail;
    //若尾节点不为空,说明当前队列已经被初始化,直接将node设为新的尾结点并更新
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //尾结点为空,说明队列尚未初始化,执行enq()方法
    enq(node);
    return node;
}

此时B、C线程同时尝试进入队列,此时队列必然还未初始化,故需要至少有一个线程执行enq(final Node node)方法初始化队列。

private Node enq(final Node node) {
	//开始进行自旋
    for (;;) {
    	//获得当前尾结点的引用
        Node t = tail;
        if (t == null) { // 如果当前尾结点为空则必须进行初始化队列操作
        	//新创建一个head结点,将tail引用指向head,此时队列中只有一个head结点。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // 当前尾结点不为空,说明队列已经被初始化
        	//将当前节点的前置结点置为当前尾结点
            node.prev = t;
            //设置当前尾结点为node结点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在这里,体现了经典的自旋+CAS操作,B和C也许会都在执行enq(final Node node)方法,但是compareAndSetHead(Node update)方法以及compareAndSetTail(Node expect, Node update)方法都是Unsafe类提供的CAS操作,所以只会有一个线程进行初始化队列,创建头结点。假设B线程创建了头结点,即初始化队列成功,B和C两个线程都会再进行第二轮循环,再假设B线程执行了compareAndSetTail(Node expect, Node update)方法后,就可以返回了,而C线程会进入第三轮循环,再执行compareAndSetTail(Node expect, Node update)方法。不过最终结果肯定是B、C线程都能成功入队。
第三步 挂起
B和C线程都去执行acquireQueued(final Node node, int arg)方法,在这个方法里,也是先尝试获取锁,获取失败则被挂起。

final boolean acquireQueued(final Node node, int arg) {
	//标记是否能够获取到锁
    boolean failed = true;
    try {
    	//标记当前线程是否被中断过
        boolean interrupted = false;
        //开始自旋
        for (;;) {
        	//获取当前结点的前驱结点
            final Node p = node.predecessor();
            //若前驱结点为head结点,说明当前结点是head的直接后继结点,便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
            	//获取成功则将当前结点设为head结点
                setHead(node);
                // 原head结点出队,帮助GC回收
                p.next = null; 
                //修改标志位,表明获取锁成功
                failed = false;
                return interrupted;
            }
            //判断当前线程获取锁失败后是否可以被挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在这里假设B、C线程在执行的过程中A线程一直占用锁,B、C线程均无法获取到锁,就会进入到第二个if语句当中,即判断当前线程获取锁失败后是否可以被挂起,若可以则挂起。观察shouldParkAfterFailedAcquire(Node pred, Node node)方法和parkAndCheckInterrupt()方法的源码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//获取前驱结点的状态
    int ws = pred.waitStatus;
    //若前驱结点状态为SIGNAL,则表明前驱结点可以唤醒后继结点,可以返回true
    if (ws == Node.SIGNAL)
        return true;
    //若前驱结点状态为CANCELLED,说明前驱结点的线程取消了等待
    if (ws > 0) {
        //一直向前遍历,直至找到一个状态为SIGNAL的结点,将当前结点挂在该结点后面
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { //将前驱结点状态设置为SIGNAL状态,即ReentranLock的初始化状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
	//阻塞当前线程
    LockSupport.park(this);
    return Thread.interrupted();
}

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,所以会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt(),将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL,即初始状态。
至此,加锁完成。

4.3 unlock解锁

解锁的源码主要是release(int arg)方法,源码如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

解锁流程对照加锁流程可以很容易地理解,主要分为尝试释放锁,若释放成功,则先获取头结点的引用,若头结点不为空并且头结点的等待状态是SIGNAL,那么可以唤醒头结点的直接后继结点所存储的线程。
接下来看一下tryrelease(int releases)方法

protected final boolean tryRelease(int releases) {
	//计算释放后的state值
    int c = getState() - releases;
    //若当前线程不是正在占用锁的线程,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    //标志是否成功释放锁
    boolean free = false;
    //成功释放锁
    if (c == 0) {
        free = true;
        //将占用锁的线程清空
        setExclusiveOwnerThread(null);
    }
    //更新state值
    setState(c);
    return free;
}

超时机制

ReentranLock类中还有一个tryLock(long timeout, TimeUnit unit)方法,该方法是用来设置一个时间阈值,在这个时间阈值范围内能获得锁,那么就返回true,若无法获取到锁,就返回false。
源码如下:

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

没什么好说的,调用的是Sync类中的tryAcquireNanos(int arg,long nanoTimeout)方法,唯一值得注意的是,时间单位是ns。
tryAcquireNanos(int arg,long nanoTimeout)方法的源码如下:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //若线程此时被中断,则抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //尝试获取锁,或者尝试等待一定的时间
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

这里就是先尝试获取锁,若此时获取锁成功,那么就可以直接返回true,若获取不到锁,就进行超时等待,执行doAcquireNanos(int arg,long nanosTimeout)方法。
doAcquireNanos(int arg,long nanosTimeout)源码如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    //设置时间阈值
    final long deadline = System.nanoTime() + nanosTimeout;
    //将线程入队
    final Node node = addWaiter(Node.EXCLUSIVE);
    //标志是否成功获取锁
    boolean failed = true;
    try {
    	//自旋
        for (;;) {
        	//获取当前结点的前驱结点
            final Node p = node.predecessor();
            //如果前驱结点是头结点且尝试获取锁成功,则将当前结点变为头结点
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //获取锁失败,计算时间阈值和当前系统时间的差值
            nanosTimeout = deadline - System.nanoTime();
            //差值小于0表示已经超时
            if (nanosTimeout <= 0L)
                return false;
            //程序执行到这里说明尚未超时,尝试将当前线程挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                //将线程阻塞直至超时
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

以上就是关于ReentranLock中的非公平锁的加锁解锁实现原理的分析。公平锁与非公平锁的区别就是,公平锁在获取锁时,不去检查state值,而是直接执行acquire(1)方法

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