[Java高并发系列(4)]Java 中 ReentrantLock 介绍 + 一道面试题
1 ReentrantLock 简介
jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock. 虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富, 使用起来更为灵活, 也更适合复杂的并发场景.
2 ReentrantLock 和 synchronized的相同功能
ReentrantLock 是独占锁 且可重入 例子:
原来我们用synchronized实现的一个例子:
import java.util.concurrent.TimeUnit;
public class ReentrantLock1 {
synchronized void m1() {
for(int i=0; i<10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
synchronized void m2() {
System.out.println("m2 ...");
}
public static void main(String[] args) {
ReentrantLock1 rl = new ReentrantLock1();
//new Thread(rl::m1).start();
new Thread(()->{
rl.m1();
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
现在改为用ReentrantLock实现:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author lowfree
*/
public class ReentrantLock2 {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock(); //synchronized(this)
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 必须要必须要必须要手动释放锁
//使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此 经常在finally中进行锁的释放锁
}
}
void m2() {
lock.lock();
System.out.println("m2 ...");
lock.unlock();
}
public static void main(String[] args) {
ReentrantLock2 rl = new ReentrantLock2();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
可重入的 也可以举个例子:
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
}
}
}
}
上面的代码通过 lock() 方法先获取锁三次, 然后通过unlock()
方法释放锁3次,程序可以正常退出. 从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。
小结: ReentrantLock和synchronized的相同点
- ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区.
但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;
ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。 - ReentrantLock和synchronized都是可重入的。synchronized 因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
3 额外功能
3.1 使用reentrantlock的 tryLock( ) 可以进行“尝试锁定”
使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行.
可以根据tryLock的返回值来判定是否锁定; 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中
看下面的例子:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock3 {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " m1 :");
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
* 可以根据tryLock的返回值来判定是否锁定
* 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中
*/
void m2() {
/*
boolean locked = lock.tryLock();
System.out.println("m2 ..." + locked);
if(locked) lock.unlock();
*/
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS); //5 秒后尝试锁定 lock对象, 返回true如果锁定成功
System.out.println("m2 ..." + locked);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock(); //如果刚刚锁定成功了, 别忘记释放锁.
}
}
public static void main(String[] args) {
ReentrantLock3 rl = new ReentrantLock3();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
上面的例程中: 创建了一个线程执行m1方法, 同时是拿这lock将其锁定的了 . 这时第二个线程起来了 , 执行m2 , 其中想在5 秒后去尝试锁定lock. 将其结果存入locked中并打印.
3. 2 使用ReentrantLock的lockInterruptibly方法,可以对线程interrupt方法做出响应
即:在一个线程等待锁的过程中,可以被打断
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法 lockInterruptibly() . 该方法可以用来解决死锁问题.
看下面这个例程:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock4 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
try {
lock.lock();
System.out.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
System.out.println("t1 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(()->{
try {
//lock.lock();
lock.lockInterruptibly(); //可以对interrupt()方法做出响应
System.out.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt(); //打断线程2的等待
}
}
分析:
- 创建线程t1 拿着锁 ,继续执行 ,然后sleep(最大int值), 这时 t2执行
- 这时线程t2 如果使用 lock.lock( ) ,则会执迷不悟地去等待t1 将lock释放, 但是这里使用lockInterruptibly( ), 虽然还是阻塞着去等待lock释放, 但是它却可以对 interrupt( )方法做出响应
- 主线程继续运行, 运行到t2 . interrupt()时 ,打断了线程2的等待.
下面看一个用这个方法解决死锁的例子:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLockInterrupt {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
static class ThreadDemo implements Runnable{
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock first , Lock secondLock){
this.firstLock = first;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+ " 正常结束!");
}
}
}
public static void main(String[] args) {
Thread th1 = new Thread(new ThreadDemo(lock1 , lock2));//该线程先获取锁1,再获取锁2
Thread th2 = new Thread(new ThreadDemo(lock1 , lock2));//该线程先获取锁2,再获取锁1
th1.start();
th2.start();
th1.interrupt();//是第一个线程中断
}
}
创建两个子线程,子线程在运行时会分别尝试获取两把锁. 其中一个线程先获取锁1再获取锁2,另一个线程正好相反.
如果没有外界中断,该程序将处于死锁状态永远无法停止. 我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。
我们也可以用之前的tryLock( ) 方法来执行解决死锁:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLockTryLock {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread2 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread1.start();
thread2.start();
}
}
线程通过调用 tryLock() 方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,**避免了两个线程分别持有一把锁然后相互请求另一把锁 . **
3.3 ReentrantLock可以实现公平锁。
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。
和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数 true 创建公平锁,如果传入的是 false 或没传参数则创建的是非公平锁
Reentrant lock = new ReentrantLock(true);
3.4 结合Condition 实现等待通知机制
使用synchronized 结合Object上的wait 和notify 方法可以实现线程间的等待通知机制. ReentrantLock 结合Condition 接口同样可以实现这个功能. 而且相比其拿着使用起来更清晰和简单.
Condition由ReentrantLock对象创建,并且可以同时创建多个
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
jdk 解释:
Condition是一个接口.
Condition ( 也称为条件队列或条件变量 )为一个线程暂停执行( “等待” )提供了一种方法,直到另一个线程通知某些状态现在可能为真。 因为访问此共享状态信息发生在不同的线程中,所以它必须被保护,因此某种形式的锁与该条件相关联。 等待条件的关键属性是它原子地释放相关的锁并挂起当前线程,就像Object.wait
。
一个 Condition 实例本质上绑定到一个锁。 Condition 实例,请使用其 new Condition( ) 方法。
4 一道面试题
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用
/**
* 写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
* 能够支持2个生产者线程以及10个消费者线程的阻塞调用
*
* 使用wait和notify/notifyAll来实现
*/
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
public class MyContainer1<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
public synchronized void put(T t) {
while(lists.size() == MAX) { //想想为什么用while而不是用if?
try {
this.wait(); //effective java
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll(); //通知消费者线程进行消费
}
public synchronized T get() {
T t = null;
while(lists.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count --;
this.notifyAll(); //通知生产者进行生产
return t;
}
public static void main(String[] args) {
MyContainer1<String> c = new MyContainer1<>();
//启动消费者线程
for(int i=0; i<10; i++) {
new Thread(()->{
for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0; i<2; i++) {
new Thread(()->{
for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}
在学了 Lock 和 Condition 后 , 也可以使用这两个来实现
/**
* 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
* 能够支持2个生产者线程以及10个消费者线程的阻塞调用
*
* 使用Lock和Condition来实现
* Condition的方式可以更加精确的指定哪些线程被唤醒
*/
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyContainerT2<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
try {
lock.lock();
while(lists.size() == MAX) { //想想为什么用while而不是用if?
producer.await();
}
lists.add(t);
++count;
consumer.signalAll(); //通知消费者线程进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while(lists.size() == 0) {
consumer.await();
}
t = lists.removeFirst();
count --;
producer.signalAll(); //通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
MyContainerT2<String> c = new MyContainerT2<>();
//启动消费者线程
for(int i=0; i<10; i++) {
new Thread(()->{
for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0; i<2; i++) {
new Thread(()->{
for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}