ReentrantLock原理

本文详细探讨了ReentrantLock与synchronized的区别,包括可打断、公平锁支持、多条件队列以及超时设置等特性。ReentrantLock基于AQS(AbstractQueuedSynchronizer)实现,内部维护同步队列和条件队列,通过CAS和park/unpark操作来管理线程。此外,还介绍了ReentrantLock的公平与非公平加锁流程,以及条件变量的await和signal方法的工作原理。

ReentrantLock与synchronized对比

  • synchronized是Java关键字,是jvm层面实现的锁,而ReentrantLock是一个Java类,基于aqs来实现,本质上是通过cas自旋+park实现锁机制,更容易在应用层面去扩展。
  • 可打断
  • 对于synchronized来说,如果一个线程无法获得锁阻塞后,它只能一直等待,其他线程无法打断它。
  • 而对于ReentrantLock它支持可打断,一个线程无法获得锁阻塞后 可以被其他线程打断继续执行。(因为park的线程是可以被打断的,block的线程无法被打断)
  • 支持公平锁
  • 对于synchronized来说它是不公平的,先申请锁的线程不一定先获得锁。
  • ReentrantLock 默认也是非公平锁,可以通过参数设置为公平锁。它是基于aqs自己实现调度,保证公平性。
  • 支持多个条件队列
  • 对于synchronized来说,它只有一个条件队列,owner线程无论是什么原因无法运行,都只能进入waitset队列。这样通过notifyAll唤醒所有线程存在虚假唤醒问题。
  • 而ReentrantLock 支持多个条件队列,可以调用每个条件变量的 await 和 signal 方法完成等待和唤醒。
  • 因为原因a等待的线程放在等待队列a,因为原因b等待的线程放在等待队列b。可以使用条件变量的 signalAll方法唤醒当前条件变量对应的等待队列中所有线程。
  • 如下表示使用条件变量进行等待和唤醒的示例 子线程1通过条件变量1阻塞,子线程2通过条件变量2阻塞,主线程通过条件变量1只能唤醒子线程1,无法唤醒子线程2,从而解决虚假唤醒问题。
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition(); //条件变量1
Condition condition2 = lock.newCondition(); //条件变量2

Thread t = new Thread(()->{
   
   
    lock.lock(); //加锁
    System.out.println("子线程1获得锁");
    try {
   
   
        condition1.await(); //通过条件变量1让 子线程陷入等待状态

        System.out.println("子线程1被唤醒");
        lock.unlock();//解锁

    } catch (InterruptedException e) {
   
   }

});
t.start();

Thread t2 = new Thread(()->{
   
   
    lock.lock(); //加锁
    System.out.println("子线程2获得锁");
    try {
   
   
        condition2.await(); //通过条件变量2让 子线程2陷入等待状态

        System.out.println("子线程2被唤醒");
        lock.unlock();//解锁

    } catch (InterruptedException e) {
   
   }

});
t2.start();

//主线程先于子线程获取锁
Thread.sleep(1000);
lock.lock(); //加锁
System.out.println("主线程获得锁");
condition1.signal(); //通过条件变量1唤醒子线程
lock.unlock();
  • 可设置超时时间
  • 一个线程获取不到锁陷入阻塞状态后,为了避免死等,可以让其他线程将其打断,这是一种被动的方式。
  • 除此之外,还可以设置超时时间,表示一个线程获取不到锁陷入阻塞状态后,如果持续了一段时间依然没有获得锁,那么退出该线程。
  • 通过设置超时时间可以很方便解决死锁问题,因为任意线程等待一段时间都会终止,就不会出现相互等待锁的情况。
  • 底层基于可超时的park方法+可打断实现。

aqs基本原理

  • AbstractQueuedSynchronizer(AQS)是抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架。
  • ReentrantLock底层就是基于aqs来实现的。
  • aqs本质上是通过park加cas自旋实现锁机制的。用一个变量state标记锁状态,以cas修改锁标记的方式进行加锁,如果多次cas尝试都加锁失败就通过park方法阻塞。
  • 我们知道对于synchronize的互斥锁来说,只要加锁失败就会陷入阻塞状态,而对于aqs,acas加锁失败不会立即陷入阻塞,而是灵活控制线程行为。
  • aqs通过自旋操作,也可以实现类似synchronize中轻量级锁和偏向锁的功能。
  • aqs的具体实现原理如下
    aqs内部维护了一个state状态,一个同步队列和若干条件队列。
volatile int state //标识锁状态
volatile Node head; //同步队列头节点
volatile Node tail; //同步队列尾节点
owner; //指向当前获得锁的线程
//多个条件队列  类似于waitset,获得锁的线程不满足某个条件陷入等待。线程同样通过park阻塞
  • 对于state变量来说标记了锁的状态,对于读写锁来说,它可以同时标记读锁和写锁两种锁的状态。另外通过state的计数器还可以记录加锁数量实现可重入锁。
  • 对于同步队列来说,当一个线程通过cas加锁失败,为了避免无限cas重试,会将其存放在同步队列并调用park方法阻塞。同步队列中的线程维护一个先后关系,占有锁的线程位于队头,当占有锁的线程释放锁时会负责唤醒第二个节点的线程。即队列每一个线程都由前一个线程负责唤醒。
  • 对于条件队列来说,默认不存在,只有设置条件变量才会用到条件队列,条件队列与同步队列结构类似,都会维护先后关系,由上一个线程负责唤醒下一个线程。条件队列存放占有锁的线程不满足某个条件陷入等待状态,也是通过park阻塞。当条件队列中线程唤醒后,不会立刻执行,而是在同步队列先排队申请锁。
  • 由于park方法不会释放锁,因此在将活动线程放入条件队列前,要把当前线程占有的锁全部释放。
  • aqs基本方法如下
//申请锁 失败会阻塞
public final void acquire(int arg) 

//申请可打断的锁
public final void acquireInterruptibly(int arg)

//尝试申请带超时的锁
public final boolean tryAcquireNanos(int arg, long nanosTimeout)

//释放锁
public final boolean release(int arg)

//申请共享锁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值