欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java 源码分析-ReentrantLock

程序员文章站 2022-07-14 16:14:05
...

  对AbstractQueuedSynchronizer分析完毕,现在来对ReentrantLock类来进行分析。在阅读本文之前,建议对AbstractQueuedSynchronizer有所了解,可以借鉴楼主的Java 源码分析-AbstractQueuedSynchronizer
  只要对AbstractQueuedSynchronizer比较熟悉,那么理解ReentrantLock就不是很难的事。因为ReentrantLock的内部就是利用AbstractQueuedSynchronizer来实现的。
  本文参考资料:

  1.方腾飞、魏鹏、程晓明的《Java 并发编程的艺术》
  2.楼主本人的Java 源码分析-AbstractQueuedSynchronizer

1.ReentrantLock的介绍

  ReentrantLock称为重入锁,顾名思义,就是支持重入的锁,它表示锁能够支持一个线程对资源进行重复的加锁。
  ReentrantLock相较于其他的锁,除了支持重入之外,还在锁的公平上有所区别。普通的锁可能就是单纯使用同步队列来实现锁的排队,而ReentrantLock在锁的定义上分为了公平锁和非非公平锁。
  也就是说,对于ReentrantLock,我们需要关注两个地方,一个是锁的重入,一个是公平性。

(1).锁重入的说明

  楼主在Java 源码分析-AbstractQueuedSynchronizer里面定义了一个Mutex类,如果现在有一个这样的场景:当一个线程调用Mutex的lock方法获取锁之后,如果再次调用lock的话,则该线程将会被自己阻塞,原因是Mutex在实现tryAcquire方法时没有考虑到占有锁的线程再次获取锁的场景,而在tryAcquire方法里面返回了false,导致该线程被阻塞。简单地说,Mutex是一个不支持重入的锁。
  而系统的synchronized关键字隐式的支持重入,在实际的开发中,我们也遇得到一个被synchronized修饰的方法内部调用了另一个被synchronized修饰的方法的情况。

(2).锁公平性的说明

  如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁就是公平的,反之,这个锁就是不公平的。公平的获取锁,也就是说等待时间最长的最先获取锁。ReentrantLock提供了一个构造函数,能够控制所是否是公平的。

2.锁重入的实现原理

  大家不要被我的题目唬住了,什么实现原理都是非常简单的。
  关于锁的重入,我们必须考虑到两个问题:
  1.线程再次获取锁。锁需要识别获取的线程是否为当前占据锁的线程,如果是的话,可以再次获取锁,反之则不能获取锁。
  2.锁的最终释放。线程重复n次获取了锁,随后在第n次释放了锁后,其他线程才能够获取到该锁。锁的最终释放要求对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放了,其他的线程可以进行进行锁的争夺了。

(1).锁的获取

  这里使用非公平性实现的为例,来解释锁获取的实现。

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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");
                setState(nextc);
                return true;
            }
            return false;
        }

  从这段代码里面,我们可以看出来,通过判断当前线程是否是获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求的话,则将同步状态值进行增加并且返回true,表示获取成功。
  成功锁的线程再次获取锁,只是增加了同步状态值,这也就是要求ReentrantLock在释放同步状态时时减少同步状态值。

(2).锁的释放

        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;
        }

  如果该锁被获取了n次,那么前 n - 1次tryRelease方法必须返回false,而只有同步状态全部释放了,才能返回true。可以看出来,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并且返回true,表示释放成功。

3.锁公平性和非公平性的区别和实现原理

  公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
  下面我贴出FairSync的tryAcquire代码,注意跟nonfairTryAcquire方法做比较。

       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;
        }

  tryAcquire方法与nonfairTryAcquire方法比较,唯一的区别在于tryAcquire方法多了一个hasQueuedPredecessors的判断条件。这个判断条件作用是,加入同步队列中当前节点是否有前驱节点,如果有的话,表示有线程比当前线程更早地请求获取锁,因此需要等待前驱获取锁并且释放锁之后才能继续获取锁。