(十六)ReentrantLock可重入锁
# 1、ReentrantLock介绍
jdk中独占锁的实现除了使用关键字synchronized
外,还可以使用ReentrantLock
。
虽然在性能上ReentrantLock
和synchronized
没有什么区别,但ReentrantLock
相比synchronized
而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
两者的相同点:
1、ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。
但是实现上两者不同:
synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;
ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
2、ReentrantLock和synchronized都是可重入的。
synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;
ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
不同点:
1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。
# 2、ReentrantLock的额外功能
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。(保证)
非公平锁则随机分配这种使用权。
和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。
在创建ReentrantLock的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁
//公平锁
ReentrantLock lock = new ReentrantLock(true);
上个例子:
public class FairReentrantLock {
// static Lock lock = new ReentrantLock(true);
static Lock lock = new ReentrantLock(false);
public static void main(String[] args) {
myThreadDemo[] threadDemos = new myThreadDemo[10];
for (int i = 0; i < 5; i++) {
threadDemos[i] = new myThreadDemo(i);
}
for (int i = 0; i < 5; i++) {
threadDemos[i].start();
}
}
static class myThreadDemo extends Thread {
int threadId;
myThreadDemo(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
try {
Thread.sleep(1 * 100);
for (int i = 0; i < 3; i++) {
lock.lock();
System.out.println("当前获得锁的线程--->>>" + threadId);
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出:
可以看到非公平锁,几乎是一个线程同时获取锁后再到下一个线程执行。
如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。
如果换成公平锁:
static Lock lock = new ReentrantLock(true);
可以看到,看起来就有那么一点打乱的顺序,系统会公平地分配资源给每个线程,而不是一个线程一直霸占着,线程几乎是轮流的获取到了锁。
# 3、ReentrantLock可响应中断问题
# synchronized
死锁例子:
class SynchronizedDeadLock implements Runnable {
private String lockA;
private String lockB;
public SynchronizedDeadLock(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
//至关重要是这个sleep,因为这里睡眠是为了让 第二个线程有机会进来
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) throws InterruptedException {
String lockA = "locka";
String lockB = "lockb";
new Thread(new SynchronizedDeadLock(lockA, lockB), "Thread1").start();
new Thread(new SynchronizedDeadLock(lockB, lockA), "Thread2").start();
}
}
# ReentrantLock
死锁例子:
public class ReentrantLockDeadLock {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
thread1.start();
thread2.start();
}
static class DeadLockDemo implements Runnable {
Lock lockA;
Lock lockB;
public DeadLockDemo(Lock lockA, Lock lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
try {
lockA.lock();
// lockA.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
TimeUnit.SECONDS.sleep(2);
lockB.lock();
// lockB.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock();
lockB.unlock();
System.out.println(Thread.currentThread().getName() + "正常结束!");
}
}
}
}
以上这两个例子都会发生死锁,它们的资源竞争是这样的:
Thread1线程执行,首先获取lockA资源,上锁,然后休眠2秒...
Thread2线程执行,然后获取lockB资源,上锁,然后休眠2秒...
Thread1线程醒来,获取lockB资源,发现被锁住了,只能等待....
Thread2线程醒来,获取lockA资源,发现被锁住了,只能等待....
这样,死锁出现了....
# ReentrantLock
可响应中断:
ReentrantLock的优点还是有的,它提供了lockInterruptibly()
方法,用于感知线程中断,从而退出程序。
public class ReentrantLockDeadLock {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
thread1.start();
thread2.start();
Thread.sleep(5 * 1000);
if (Thread.activeCount() >= 4) {
thread1.interrupt();//让thread1线程中断
}
}
static class DeadLockDemo implements Runnable {
Lock lockA;
Lock lockB;
public DeadLockDemo(Lock lockA, Lock lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
try {
// lockA.lock();
lockA.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
TimeUnit.SECONDS.sleep(2);
// lockB.lock();
lockB.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock();
lockB.unlock();
System.out.println(Thread.currentThread().getName() + "正常结束!");
}
}
}
}
结果:
程序在休眠5秒后,假如Thread.activeCount() >= 4
,主线程、守护线程、Thread1、Thread2 四个都在, 表示死锁发生了。
通过 thread1.interrupt()
,中断了Thread1
,lockInterruptibly()
方法 会释放lockA
、lockb
的锁,即 lockA.unlock()
、lockB.unlock()
;Thread2
感知了,就可以获取loackB
的资源,即可以上锁,然后正常退出。
所以ReentrantLock
相比synchronized
的优势就是:无限等待获取锁的行为可以被中断
# 4、ReentrantLock锁限时等待
线程中断不是处理死锁特别好的方法,万一线程真的是执行了很久,而不是死锁了,如果贸然中断,可不是一个明智的处理方法。
ReentrantLock 提供了一个tryLock()
方法,可以指定获取锁的等待时间。
tryLock()
如果拿到锁就返回true,否则返回false,不会像lock那样无限等待。
if (!lockA.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 正在等待锁......");
} else {
System.out.println(Thread.currentThread().getName() + " 拿到了锁");
}
tryLock()
也会有死锁的情况,所以为了避免死锁,一个线程不要获取多个锁。
# 5、Condition
synchronized可以结合Object进行线程之间的通信,比如说wait
和notify
实现线程的等待和唤醒。
ReentrantLock也有,Java提供了Condition 接口,可以实现ReentrantLock线程之间的通信。
eg:
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
//通过ReentrantLock创建Condition实例,并与之关联
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程执行ing...");
new Thread(new AwaitThread()).start();
try {
Thread.sleep(2000);
lock.lock();
condition.signal();
} finally {
lock.unlock();
}
System.out.println("主线程执行结束");
}
static class AwaitThread implements Runnable {
@Override
public void run() {
System.out.println("子线程执行ing...");
lock.lock();
try {
System.out.println("子线程停止了");
condition.await();
System.out.println("子线程恢复执行了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
结果:
主线程执行ing...
子线程执行ing...
子线程停止了
主线程执行结束
子线程恢复执行了
# 6、ReentrantLock底层原理
ReentrantLock底层使用了CAS
+AQS队列
(也叫CLH队列
)实现:
# 6.1、 CAS(Compare and Swap)
CAS,Compare and Swap,比较并交换,是一种无锁算法。
CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe
这个类通过JNI调用CPU底层指令实现。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则重新获取内存地址V的当前值,并重新计算想要修改的值(重新尝试的过程被称为自旋)。
# 6.2、AQS队列
AQS是一个用于构建锁和同步容器的框架。
AQS使用一个FIFO的队列(也叫CLH队列),表示排队等待锁的线程。
也称为CLH队列:带头结点的双向非循环链表(如下图所示):
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
- 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
- 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
# 总结:
ReentrantLock比起synchronized功能更加丰富,支持公平锁和非公平锁,而且提供了响应中断。
而且提供了tryLock()
锁限时等待,相比synchronized要更灵活。
参考: