1.ReentrantLock的概念
ReentrantLock
是 可重入[1] 的互斥锁,虽然与synchronized
相似,但是会比synchronized
更加灵活(具有更多的方法),ReentrantLock
底层基于AbstractQueuedSynchronizer
(AQS)[2] 实现。在ReentrantLock
中默认的策略是非公平锁[3]
概念解释:
[1]什么是可重入锁?
synchronized 和 ReentrantLock 都是可重入锁,指的是当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁
[2]什么是AQS?
AbstractQueuedSynchronizer
抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题。
[3]公平锁和非公平锁分别是什么?
举个例子:我早上去买早餐,排队的人和我都是有素质的文明人,大家老老实实排队,晚来的到队尾排队,这样大家都觉得很公平,先到先得,所以这是公平锁。非公平锁就是我变成了一个凶残霸道的人,上来就挤到第一位前面,但偶尔会碰到硬茬子,让我滚到后面排队去。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死
2.synchronized和ReentrantLock的比较:
2.ReentrantLock的使用
lock()获取锁,unlock()释放锁
传统的synchronized代码:
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
synchronized ("lock"){
System.out.println("线程"+Thread.currentThread().getName()+"获得了锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1=new Thread(runnable);
Thread t2=new Thread(runnable);
t1.start();
t2.start();
}
如果用ReentrantLock替代,可以把代码改造为:
public static void main(String[] args) {
Runnable runnable = new Runnable() {
//实例化ReentrantLock
final Lock lock = new ReentrantLock();
@Override
public void run() {
//加锁
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "获得了锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放锁
lock.unlock();
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
3.tryLock()尝试获取锁
和synchronized不同的是,ReentrantLock可以尝试获取锁。
tryLock() : 尝试获取锁,一定时间后,还没有持有锁, 则自动中断
public static void main(String[] args) {
ReentrantLock lock1=new ReentrantLock();
ReentrantLock lock2=new ReentrantLock();
new Thread(()->{
if (lock1.tryLock()){
System.out.println("A-lock1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock2.tryLock()){
System.out.println("A-lock2");
}else {
System.out.println("A获取锁失败...");
}
}
if (lock2.isHeldByCurrentThread()){lock2.unlock();}
if (lock1.isHeldByCurrentThread()){lock1.unlock();}
},"A").start();
new Thread(()->{
if (lock2.tryLock()){
System.out.println("B-lock2");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock1.tryLock()){
System.out.println("B-lock1");
}else {
System.out.println("B获取锁失败...");
}
}
if (lock2.isHeldByCurrentThread()){lock2.unlock();}
if (lock1.isHeldByCurrentThread()){lock1.unlock();}
},"B").start();
}
上述代码在尝试获取锁的时候,如果等待一段时间后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
4.lockInterruptibly()中断锁
以下代码会出现死锁
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread t1 = new Thread() {
@Override
public void run() {
try {
lock1.lock();
System.out.println("A--lock1");
lock2.lock();
System.out.println("A--lock2");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (lock2.isHeldByCurrentThread()) lock2.unlock();
if (lock1.isHeldByCurrentThread()) lock1.unlock();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
lock2.lock();
System.out.println("B--lock2");
lock1.lock();
System.out.println("B--lock1");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) lock1.unlock();
if (lock2.isHeldByCurrentThread()) lock2.unlock();
}
}
};
t1.start();
t2.start();
但是当线程获取了中断锁的时候, 其他线程就可以中断这把锁
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread t1 = new Thread() {
@Override
public void run() {
try {
lock1.lockInterruptibly();
System.out.println("A--lock1");
lock2.lockInterruptibly();
System.out.println("A--lock2");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (lock2.isHeldByCurrentThread()) lock2.unlock();
if (lock1.isHeldByCurrentThread()) lock1.unlock();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
lock2.lockInterruptibly();
System.out.println("B--lock2");
lock1.lockInterruptibly();
System.out.println("B--lock1");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) lock1.unlock();
if (lock2.isHeldByCurrentThread()) lock2.unlock();
}
}
};
t1.start();
t2.start();
Thread.sleep(5000);
t1.interrupt();
}
5.源码简单理解
查看JDK的源码(JDk1.8),可以看出ReentrantLock默认是非公平锁,如果在构造方法当中传入TRUE,则使用公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
...........
...........
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
而如果继续观察NonfairSync()
和FairSync()
,则会发现NonfairSync
和FairSync
都继承自Sync
static final class NonfairSync extends Sync {
............
............
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...........................
............................
static final class FairSync extends Sync {
............
............
final void lock() {
acquire(1);
}
····················
····················
而Sync
继承自AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
..........
...........
abstract void lock();
.........
.........
.........
通过分析源码,可知对ReentrantLock
的操作都转化为对Sync
对象的操作,由于Sync
继承了AQS,所以基本上都可以转化为对 AQS的操作。如将ReentrantLock
的 lock()
转化为对 Sync
的 lock()
的调用,而具体会根据采用的是NonfairSync
还是FairSync
而调用不同的对lock()
实现。
public void lock() {
sync.lock();
}
而unlock()
则是调用的release(),这是AQS当中的方法
public void unlock() {
sync.release(1);
}