独占锁ReentrantLock原理解析

1 基本介绍

ReentrantLock是可重入独占锁,同时只能由一个线程锁持有,如果其他线程想要获取锁,就会被阻塞并放入该锁的AQS同步队列中。

1.1 为什么要使用ReentrantLock

在多线程编程中,如果要多资源进行加锁,最常用的方法是使用synchronized关键字,然而synchronized关键字缺有诸多不足之处,而ReentrantLock相对而言可以解决这些不足,如:

  • synchronized无法知道线程有没有成功获取到锁,而ReentrantLock可以。
  • synchronized的 wait方法只能有一个条件变量,而ReentrantLock可以配合Condition的await方法实现多个条件变量,更为灵活。

1.2 ReentrantLock的常用方法

下面就是介绍一下ReentrantLock的常用方法:

方法名 作用
lock() 获取锁,成功后变返回,如果失败了,就会被阻塞
lockInterruptibly() 与lock()方法作用一样,不同的是该方法可以对响应,如果其他线程调用当前线程的interrupt() 方法,则当前线程会抛出InterruptedException异常并返回
tryLock() 在不阻塞当前线程的情况下,尝试获取锁,成功则返回true,失败则返回false
tryLock(long timeout, TimeUnit unit) 如果在指定的时间内未能成功获取锁,则返回false,成功则返回true
unlock() 状态值减去1,如果减1后状态值为0则释放锁
newCondition() 获取一个条件变量

2 源码梳理

这里还是一样的用的 Jdk11,和网上一些以 Jdk8的资料有一些差异,但是差异不大。

2.1 构造方法

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

由构造方法可知,ReentrantLock默认为非公平锁,因为这样获取锁时省略了一些检查操作,速度快一些。如果想要使用公平锁,可以在创建ReentrantLock时传入true即可。

2.2 加锁

方法入口如下:

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

sync是Sync对象,Sync继承了AbstractQueuedSynchronizer,所以sync.acquire(1)实际的调用了AbstractQueuedSynchronizer的acquire方法,下面进入acquire方法。

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

acquire方法中做了2个判断,第一个判断是尝试获取锁,如果失败了则进入第二个判断,将当前线程封装为类型为Node.EXCLUSIVE的Node节点,插入AQS同步队列的尾部,并不停的循环获取锁。
另外,AQS并不实现tryAcquire方法,而是由子类实现,所以我们进入FairSync的tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
	// 具体实现方法是 nonfairTryAcquire。
    return nonfairTryAcquire(acquires);
}

// 以非公平的方式获取锁
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取 AQS状态值
    int c = getState();
    // 如果 AQS状态值为 0,则说明当前锁未被占用,则可以获取锁。
    if (c == 0) {
    	// 这里主要是执行的 AQS的操作,以 CAS的方式将 AQS状态值设置为 1。
    	// 非公平锁和公平锁的差异主要是体现在这里。
        if (compareAndSetState(0, acquires)) {
        	// 如果成功将 AQS状态值,则将 AQS中锁的持有者设置为当前线程。然后返回。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果锁已经被占用了,那么就先判断当前线程是否是该锁的持有者。
    else if (current == getExclusiveOwnerThread()) {
    	// 如果是,则将 AQS的状态值加 1
        int nextc = c + acquires;
        // int类型是有上限的,这里主要是防止可重入次数溢出。
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 重新设置 AQS状态值并返回。
        setState(nextc);
        return true;
    }
    return false;
}

以上是非公平锁的实现方式,另外试想一下,如果当前锁被线程A所持有,AQS同步队列中有一个线程B正在等待锁,正常的情况是A释放锁,然后B通过CAS的方式正常获取锁,但是如果B获取锁的时,恰好有一个线程C也通过CAS的方式获取锁,并且成功获取锁了,那么B将又被方法 AQS同步队列,继续挂起。这样对于B而y言是不公平的,因为它等待的时间很久,反而被C“插队”了。
接下来我们看一下公平锁是如何实现的:

// 以公平的方式获取锁
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = this.getState();
    if (c == 0) {
    	// 差异主要在这里,多了一个 hasQueuedPredecessors 判断。
        if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, acquires)) {
            this.setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == this.getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) {
            throw new Error("Maximum lock count exceeded");
        }

        this.setState(nextc);
        return true;
    }

    return false;
}

由此我们可以看出,公平锁与非公平锁的差异是在于公平锁多了一个 !this.hasQueuedPredecessors()判断,我们可以进入该判断看一下:

public final boolean hasQueuedPredecessors() {
    Node h, s;
    // 如过 AQS同步队列的前驱节点(head)不是空
    if ((h = head) != null) {
    	// 根据前驱节点(head)获取下一个节点,也就是 AQS同步队列中的第一个节点,然后判断其是否为空。
    	// 如果 h.next不是空,说明 AQS同步队列中已经有线程等待获取锁中。那么就接着判断该节点的线程是否调用了条件变量的 await方法将自己挂起。
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null;
            // 遍历 AQS同步队列,直到找到一个未调用条件变量的 await方法将自己挂起的线程。
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        // 如果 AQS同步队列中存在一个未调用过 await的线程,并且这个线程并非是当前线程,则返回true。
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    return false;
}

由此方法便可以看出,如果 AQS同步队列中存在可以正常获取锁的线程,那么新来的线程就无法“插队”。
接下来我们便进入如果调用tryAcquire方法失败,而进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的方法。进入addWaiter方法:

private Node addWaiter(Node mode) {
    //创建一个类型为 Node.EXCLUSIVE的Node节点
    Node node = new Node(mode);
	// 将其加入 AQS同步队列的尾部。
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
        	// 初始化同步队列,在这个方法中,是创建一个Node节点为哨兵节点,并设置为 AQS前驱节点(head)。
            initializeSyncQueue();
        }
    }
}

接下来我们便进入acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
    	// 循环的方式获取锁
        for (;;) {
        	// 获取当前线程的前置节点
            final Node p = node.predecessor();
            // 如果当前线程的前一个(pre)节点就是 AQS的前驱节点(head),那么说明“轮到”这个线程去获取锁了
            // 然后调用 tryAcquire方法获取锁
            if (p == head && tryAcquire(arg)) {
            	// 将 AQS的前驱节点(head)设置为当前节点,可以看出这是懒加载的形式。
                setHead(node);
                p.next = null;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

由此,线程便成功获取锁了。

2.3 释放锁

释放锁就相对而言毕竟容易了,下面我们看一下代码:

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


public final boolean release(int arg) {
	// 具体逻辑由 AQS子类实现
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
	// 将 AQS状态值减去 1(默认为1)
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
    	// 如果当期线程不是锁的拥有者,就会报错,所以在未获得锁之前,切勿调用锁的 unlock方法
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果状态值减去1后为0,则可以释放锁。
    if (c == 0) {
        free = true;
        // 将当前锁的拥有者设置为空
        setExclusiveOwnerThread(null);
    }
    // 设置 AQS状态值
    setState(c);
    return free;
}

这里需要注意的是,因为是可重入锁,同一线程每获取锁一次状态值就会加1,所以解锁也应该解同样的次数,否则仅仅是状态值减1,不会释放锁。

参考

  1. 电子工业出版社,翟陆续,薛宾田著,《Java并发编程之美》。
  2. RedSpider社区,《深入浅出Java多线程》。
发布了8 篇原创文章 · 获赞 3 · 访问量 279

猜你喜欢

转载自blog.csdn.net/quanhong_ding/article/details/105324716