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 ( ) ;
Condition condition2 = lock. newCondition ( ) ;
Thread t = new Thread ( ( ) -> {
lock. lock ( ) ;
System. out. println ( "子线程1获得锁" ) ;
try {
condition1. await ( ) ;
System. out. println ( "子线程1被唤醒" ) ;
lock. unlock ( ) ;
} catch ( InterruptedException e) {
}
} ) ;
t. start ( ) ;
Thread t2 = new Thread ( ( ) -> {
lock. lock ( ) ;
System. out. println ( "子线程2获得锁" ) ;
try {
condition2. await ( ) ;
System. out. println ( "子线程2被唤醒" ) ;
lock. unlock ( ) ;
} catch ( InterruptedException e) {
}
} ) ;
t2. start ( ) ;
Thread. sleep ( 1000 ) ;
lock. lock ( ) ;
System. out. println ( "主线程获得锁" ) ;
condition1. signal ( ) ;
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;
对于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 boo