Java可重入锁ReentrantLock

1. ReentrantLock的概念

ReentrantLock是一个可重入的独占锁(/互斥)锁。

  • 可重入:指任意线程在获取到锁之后能够再次获取该锁而不会被阻塞。
  • 独占:每次只能有一个线程能持有锁;与之相应的时共享锁,则允许多个线程同时获取锁,并发访问,共享资源,ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁是独占的。

ReentrantLock继承了Lock接口,其内部类Sync继承了队列同步器AQS,Sync有两个子类:公平锁FairSync和非公平锁NonfairSync。在"公平锁"的机制下,线程依次排队获取锁;而"非公平锁"在锁是可获取状态时,不管自己是否在同步队列的队头都会获取锁。"公平锁"保证了锁的获取按照FIFO原则,而代价则是进行大量的线程切换,耗时多,开销大;"非公平锁"虽然可能导致线程饥饿,但却有极少的线程切换,保证了其更大的吞吐量。下面从源码的角度重点分析ReentrantLock的可重入、公平锁和非公平锁。

2. ReentrantLock的可重入

可重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题:

线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取,同步状态自增;
锁的最终释放:锁释放时,同步状态自减,线程重复 n 次获取了锁,需要进行 n 次释放锁,当同步状态等于 0时,锁释放成功,其它线程才能够获取到该锁。
所以只要分析清楚了ReentrantLock获取锁和释放锁的原理,就分析清楚了可重入。

3. 获取锁

获取锁和释放锁的过程,其实就是获取同步状态和释放同步状态的过程。子类继承AQS之后,需要重写tryAcquire()tryRelease()tryAcquireShared()tryReleaseShared()isHeldExclusively()方法,它们的功能分别是 以独占方式获取锁、以独占释放锁、以共享方式获取锁、以独占方式释放锁 和 判断同步状态是否被当前线程独占。ReentrantLcok是独占锁,所以只需要重写tryAcquire()tryRelease()isHeldExclusively()方法即可。

ReentrantLock分为公平锁和非公平锁,它们对获取锁的方式不一样,也就是tryAcquire()方法的实现不一样,所以公平锁类FairSync和非公平锁NonfairSync要分别重写该方法;而它们释放锁的方式一样,所以tryRelease()方法在两者的公共父类Sync中重写,isHeldExclusively()方法也在Sync中重写。

3.1 公平锁

队列同步器AQS的内部维持着一个FIFO双向等待队列,公平锁的获取顺序符合FIFO原则,如果当前同步状态为0,需要调用hasQueuedPredecessors()方法判断同步队列中当前节点是否有前驱节点;如果当前同步状态不为0,需要判断已获取锁的线程是否为当前线程,如果是,则同步状态自增。公平锁FairSync的tryAcquire(int)方法如下所示:

/**
     * 以公平方式获取锁
     */
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取同步状态
        int c = getState();
        // 同步状态为0,表示没有线程获取锁
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程
        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()方法主要是对同步队列中当前节点是否有前驱节点进行判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,其源码如下所示:

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        // 同步队列尾节点
        Node t = tail; // Read fields in reverse initialization order
        // 同步队列头节点
        Node h = head;
        Node s;
        return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
    }
3.2 非公平锁

非公平锁的实现在Sync的nonfairTryAcquire()方法中,与公平锁比较,唯一不同就是非公平锁不需要进行hasQueuedPredecessors()判断,源码如下图所示:

/**
     * 以非公平方式获取锁
     */
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取同步状态
        int c = getState();
        // 同步状态为0,表示没有线程获取锁
        if (c == 0) {
            // 执行CAS操作,尝试修改同步状态
            if (compareAndSetState(0, acquires)) {
                // 同步状态修改成功,获取到锁
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程
        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;
    }

4. 释放锁

公平锁和非公平锁的释放锁的方法tryRelease()都自继承父类Sync,也就是tryRelease()方法在Sync重写。获取锁时,同步状态自增;释放锁时,同步状态自减;ReentrantLock是可重入锁,如果线程重复获取 n 次锁,就需要释放 n 次锁,即同步状态为0,这样释放锁才成功,其它等待的线程才可以获取同步状态,tryRelease()源码如下:

protected final boolean tryRelease(int releases) {
        // 计算新的状态值
        int c = getState() - releases;
        // 判断当前线程是否是持有锁的线程,如果不是的话,抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 新的状态值是否为0,若为0,则表示该锁已经完全释放了,其他线程可以获取同步状态了
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        // 更新状态值
        setState(c);
        return free;
    }

5. ReentrantLock与synchronized的区别与联系

区别:

  • ReentrantLock是JDK类层面实现;synchronized是JVM层面实现。
  • ReentrantLock增加了一些高级功能,主要以下三项:等待可中断、可实现公平锁及可以绑定多个条件(一个ReentrantLock对象可以同时绑定多个Condition对象)。

联系:

  • ReentrantLock与synchronized都是可重入锁,同一线程反复进入同步块也不会出现自己把自己锁死的情况。

JDK 6或以上版本,性能已经不再是选择synchronized或者ReentrantLock的决定因素。基于以下理由,我们仍然推荐在synchronized与ReentrantLock都可满足需要时优先使用synchronized:

  • synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐synchronized。
  • Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。
  • 尽管在JDK 5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利了。从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用J.U.C中的Lock的话,Java虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ReentrantLock)是一种独占,也就是说同一时间只能有一个线程持有该。与 synchronized 关键字不同的是,可以支持公平和非公平两种模式,而 synchronized 关键字只支持非公平的实现原理是基于 AQS(AbstractQueuedSynchronizer)框架,利用了 CAS(Compare And Swap)操作和 volatile 关键字。 的核心思想是“可性”,也就是说如果当前线程已经持有了该,那么它可以复地获取该而不会被阻塞。在内部,使用了一个计数器来记录当前线程持有该的次数。每当该线程获取一次时,计数器就加 1,释放一次时,计数器就减 1,只有当计数器为 0 时,其他线程才有机会获取该的基本使用方法如下: ```java import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-2").start(); } } ``` 在上面的示例代码中,我们创建了两个线程,分别尝试获取。由于支持可性,因此第二个线程可以成功地获取到该,而不会被阻塞。当第一个线程释放后,第二个线程才会获取到并执行相应的操作。 需要注意的是,使用时一定要记得在 finally 块中释放,否则可能会导致死的问题。同时,在获取时也可以设置超时时间,避免由于获取失败而导致的线程阻塞问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值