ReentrantLock原理 JDK8

7 篇文章 0 订阅

ReentrantLock (简称RS)是区别于 synchronized 的一种锁技术,特性有 等价于 synchronized 的 lock、可重入锁、公平锁与非公平锁、读锁与写锁、Condition对象等等......

RS应该在同步性能上与 synchronized 到底孰优孰劣,未知。

不过由于 RS基于 volatile 变量 state 实现锁的机制,而 synchronized 是基于JVM的 mointer 对象实现的锁机制(偏向锁、轻锁不再考虑范围内)。各位可以自己推敲或试验下。

RS任然是排它锁,即在同一个单位时间内只能有一个线程持有锁(一个线程执行同步代码块)。

ReentrantReadWriteLock则是读写锁,读共享,读与写互斥,区分了读与写比RS更加细粒化。

Lock lock = new ReentrantLock(true);

Condition condition = lock.newCondition();

通常学习一个类,要从它的结构和静态变量,静态方法开始,这个对象也不例外。

看结构图得知,它实现了Lock接口,内部有个抽象的静态类 Sync(简称 S),S是实现各种特性的关键类,S下面有2个具体的实现子类,FairSync、NonFairSync,公平锁与非公平锁。

S 继承了 AbstractQueueSynchronizer (简称AQS),AQS是一个基类,里面有各种基础方法提供同步,同样也是很重要的类。

默认构造的RS是非公平锁,非公平锁比公平锁更高效一点,synchronized 关键字本身也是非公平的模式。

特性1:

等价于 synchronized 的 lock。RS的 lock 有2种,一种是 lock(),如果不成功就会阻塞,成功则返回true。另外一种是 tryLock(),不成功不会阻塞,返回false。

NonFairSync(简称NFS),NFS是先尝试CAS将state置为1,即当前已加锁1次,然后再将当前线程置为持有排它锁的线程。否则会调用AQS的 acquire()。

FairSync (简称FS),则是直接调用AQS.acquire()。

AQS的 acquire 可看到是调用 tryAcquire 尝试获取锁,实际上是由子类 RS中的 FS 和 NFS 复写了 tryAcquire 方法。即 RS 的 lock 直接获取锁的动作实际上也是 tryAcquire 方法。只不过加入了失败阻塞的操作。

如果失败,会将当前线程包装成Node 放入队列的尾部,然后在 acquireQueued() 中死循环获取

锁,此时线程在排队过程中是被公平唤醒和外面的线程竞争锁。只有线程变成了头结点时才被唤醒

再次调用tryAcquire方法获取锁,成功了则把自己设置成头结点并唤醒下一个节点。否则挂起自己。

观摩unlock()操作,实际是调用tryRelease()释放锁:

tryRelease 在 Sync 中被复写,逻辑很简单。还是巧妙运用 volatile 修饰的 state,如果为0了,代表锁被释放完全了,将持有锁的线程置空,再CAS设置state。

思考:此时若线程在 setState() 停止了,其它线程去获取锁,能获取到吗?

答案是不能,因为获取锁的逻辑会判断 c 是否为 0 ,此时c还没被置为0,同样获取锁的线程也不会等于null,所以获取不到锁。

使用lock()时应注意始终将同步代码块加finally unlock(),如:

lock.lock();

try{

some codes...

}

finally{

lock.unlock();

}

因为难保try中的代码异常,此时就要将锁释放掉。否则就造成当前的执行线程一直持有锁,其它线程会被阻塞,造成大问题。

特性2:

可重入性,是指当前持有锁的线程可再次进入lock的代码块,即再次获取锁。同时state会被自增1,当解锁时state也需被减到0,才代表锁已被完全释放,否则其他线程是进入不了lock代码块的。

它是靠 state 变量实现的。state变量是被 volatile 修饰的,保证了内存可见性。

可重入性 FS 和 NFS 实现稍微有区别,

NFS:可看到不会管队列中的等待线程,直接CAS获取锁,抢占模式。

FS:会考虑队列中若有先到的线程,则会加锁失败。

值得注意的是 hasQueuedPredecessors() 函数是判断等待队列中有无线程。

这个等待队列是AQS维护的,所有尝试获取锁的线程失败后都会被包装成Node,加入AQS等待队列的尾部。

特性3:

公平锁和非公平锁,分别指严格按照加锁的时间顺序获得锁与抢占模式获得锁。

非公平锁会比公平锁更加高效一点。非公平锁有可能刚放弃完锁,再一次进入lock代码块时又立即获得了锁,有可能造成 “饥饿” 现象。公平锁模式下,刚放弃完锁的线程不会立即获得锁,除非队列中没有其它线程。

synchronized 从这点来看是非公平的,因为A线程刚执行完 synchronized ,再次进入

synchronized 时有可能立即再一次获得锁。

特性4:

condition对象,Condition是一个接口,ReentrantLock获得的是ConditionObject对象,ConditionObject 实现了 Condition接口。

Condition可以理解为一个 等待和通知组件。等价于 Object 锁的 wait() notify() notifyAll()。

Condition是与相关的Lock被绑定使用的,Condition必须配合Lock一起使用,调用await()、singal()等方法必须获得与此Condition对象相关的Lock。否则会抛出 IllegalMonitorStateException。

首先要明白一点,AQS的等待队列和 ConditionObject 的等待队列是2个不同的队列,

其中AQS的队列是竞争锁导致被阻塞的队列(简称竞争队列),

ConditionObject 的队列是线程主动调用 await() 方法放弃锁,进入等待状态的队列。(简称等待队列)

学习await()函数,大致实现流程:

先看图,然后结合代码思考一下,就比较容易理解了。

AQS 等待队列与 Condition 队列是两个相互独立的队列

  1. #await() 就是在当前线程持有锁的基础上释放锁资源,并新建 Condition 节点加入到 Condition 的队列尾部,阻塞当前线程 。
  2. #signal() 就是将 Condition 的头节点移动到 AQS 等待节点尾部,让其等待再次获取锁。

以下是 AQS 队列和 Condition 队列的出入结点的示意图,可以通过这几张图看出线程结点在两个队列中的出入关系和条件。

I.初始化状态:AQS等待队列有 3 个Node,Condition 队列有 1 个Node(也有可能 1 个都没有)

II.节点1执行 Condition.await()

  1. 将 head 后移
  2. 释放节点 1 的锁并从 AQS 等待队列中移除
  3. 将节点 1 加入到 Condition 的等待队列中
  4. 更新 lastWaiter 为节点 1

III.节点 2 执行 Condition.signal() 操作

  1. 将 firstWaiter后移
  2. 将节点 4 移出 Condition 队列
  3. 将节点 4 加入到 AQS 的等待队列中去
  4. 更新 AQS 的等待队列的 tail

public final void await() throws InterruptedException {

    // 当前线程中断

    if (Thread.interrupted())

        throw new InterruptedException();

    // 当前线程加入等待队列

    Node node = addConditionWaiter();

    // 释放锁

    long savedState = fullyRelease(node);

    int interruptMode = 0;

    /**

     * 检测此节点的线程是否在竞争队列上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待

     * 直到检测到此节点在竞争队列上

     */

    while (!isOnSyncQueue(node)) {

        // 线程挂起

        LockSupport.park(this);

        // 如果已经中断了,则退出

        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

            break;

    }

    // 竞争锁

    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)

        interruptMode = REINTERRUPT;

    // 清理下条件队列中的不是在等待条件的节点

    if (node.nextWaiter != null) // clean up if cancelled

        unlinkCancelledWaiters();

    if (interruptMode != 0)

        reportInterruptAfterWait(interruptMode);

}

结合代码可以理解:

1,调用await()的线程包装成Node,加入了 ConditionObject 的等待队列,且是队列尾部。

2,释放锁。

3,将线程挂起,这里的 LockSupport.park(this),是本地方法实现,是由操作系统实现的。

4,死循环,期间一直会有线程不断唤醒等待队列中的线程,先移出等待队列再将它们加入到竞争队列中。这个过程持续到当前线程变成等待队列的头部的时候,然后由某个线程 signal 唤醒它,同样地它也被移除等待队列加入到竞争队列中,此时循环终止。

5,开始竞争锁,即死循环竞争锁,线程又再次被阻塞。

6,直到线程恢复自由时,即回到业务代码中的时候,线程此时肯定是重新获取到了锁的。

awaitNanos() 方法:

AQS的 这个等待超时方法和 上面的 await() 差不多,不过差别就在等待时间上,可看到被加入等待队列尾部后,也是循环判断线程是否在竞争队列中。

不在就由 OS 暂时挂起线程,时间是精确到纳秒级的,当指定的等待时间耗尽后,仍然没有加入竞争队列时,就会放弃等待。进入 transferAfterCancellWait() 方法,直接加入竞争队列,然后跳出循环,开始竞争锁。

若当前节点有下一个线程在等待,因为当前节点已经被标记为放弃等待了,所以进入 unlinkCancelledWaiters() 这个方法会清除等待队列中所有已放弃等待的节点。

所以这个方法比起 await() 相当于走捷径的,等待一定时间后直接进入竞争队列,而不用辛苦排队到等待队列的头部才被加入竞争队列。

学习signal()方法:

该函数就是将等待队列中的头结点加入到竞争队列尾部。

final boolean transferForSignal(Node node) {

    //将该节点从状态CONDITION改变为初始状态0,

    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))

        return false;


    //将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点

    Node p = enq(node);

    int ws = p.waitStatus;

    //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒

    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

        LockSupport.unpark(node.thread);

    return true;

}

整个通知的流程如下:

  1. 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
  2. 如果线程已经获取了锁,则将唤醒条件队列的首节点
  3. 唤醒首节点是先将条件队列中的头节点移出,然后调用 AQS 的 #enq(Node node) 方法将其安全地移到 CLH 同步队列中
  4. 最后判断如果该节点的同步状态是否为 Node.CANCEL ,或者修改状态为 Node.SIGNAL 失败时,则直接调用 LockSupport 唤醒该节点的线程。

最后是 Condition 的生产者、消费者的经典场景使用:

通过代码看到,同一个锁衍生出2个条件。分别代表2种情况:有资源可消费,无资源可消费。

当生产者往容器加东西,当触发到某个条件如容器满了,就进入 有资源状态 的等待队列。等待资源被其它线程消费。

当消费者消费资源,触发某个条件,如资源不足就进入 缺乏资源状态 的等待队列,等待资源被生产。

顺利的情况下,生产者生产资源后未触发条件,此时有资源可消费,则唤醒一个 有资源状态 中等待的线程,让消费者竞争锁,去消费资源。

同理,消费者也是如此,在每一次消费一个资源后,都会唤醒一个生产者竞争锁,让它生产资源。

public class ConditionTest {

    private LinkedList<String> buffer;    //容器

    private int maxSize ;           //容器最大

    private Lock lock;

    private Condition fullCondition;

    private Condition notFullCondition;


    ConditionTest(int maxSize){

        this.maxSize = maxSize;

        buffer = new LinkedList<String>();

        lock = new ReentrantLock();

        fullCondition = lock.newCondition();

        notFullCondition = lock.newCondition();

    }


    public void set(String string) throws InterruptedException {

        lock.lock();    //获取锁

        try {

            while (maxSize == buffer.size()){

                notFullCondition.await();       //满了,添加的线程进入等待状态

            }


            buffer.add(string);

            fullCondition.signal();

        } finally {

            lock.unlock();      //记得释放锁

        }

    }


    public String get() throws InterruptedException {

        String string;

        lock.lock();

        try {

            while (buffer.size() == 0){

                fullCondition.await();

            }

            string = buffer.poll();

            notFullCondition.signal();

        } finally {

            lock.unlock();

        }

        return string;

    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ReentrantLock是Java中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLock是Java中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLock是Java中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值