JUC可重入锁ReentrantLock
1.概述
java.util.concurrent.locks.ReentrantLock,即可重入锁,是Lock接口的一个实现类,可以实现和synchronized一样的功能
以往我们都是使用synchronized解决线程的安全问题,在JUC中,java.util.concurrent.locks.Lock接口提供了比synchronized更多的功能,可以使用Lock接口实现手动上锁和释放锁,并且可以更加精准的设置条件(Condition)控制等待唤醒,它的特点以及和synchronized的区别是:
- synchronized是Java中的关键字,而Lock则是Java中的一个类。
- synchronized的上锁和释放锁都是自动完成,而使用Lock时必须手动释放锁,否则会造成死锁现象。
- Lock可以让等待锁的线程中断,而synchronized却不能,使用synchronized时,未得到锁的线程会一直等待下去不会中断。
- 通过Lock可以知道是否成功获取到锁,而synchronized却不能。
- Lock可以提高多个线程进行读操作的效率,在大量线程同时竞争时,效率远胜于synchronized。
2.获取锁和释放锁
使用lock.lock();方法实现上锁,用lock.unlock();方法释放锁,一般情况下,lock.unlock();的代码总是要写在finally中,以保证方法出现异常后也能释放锁,避免死锁。
例:10个线程卖票,每次只能有一个线程对票数进行修改,JUC中所有的例子都可以写成“线程操作资源类”的形式,本例创建10个线程共同操作资源类TicketTask,调用ticket()方法卖票
package example.juc.test2;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestLock { public static void main(String[] args) { TicketTask ticketTask = new TicketTask(); for (int i = 0; i < 10; i++) { new Thread(() -> { ticketTask.ticket(); }, "线程" + i).start(); } }}class TicketTask { private int number = 500; private Lock lock = new ReentrantLock(); public void ticket() { while (true) { lock.lock(); try { if (number > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } number--; System.out.println(Thread.currentThread().getName() + "完成售票:" + number); } else { break; } } finally { lock.unlock(); } } }}3.公平与非公平
synchronized默认是非公平的,ReentrantLock也是默认非公平锁,比如以下例子运行结果会出现卖的票被同一个线程大量抢占的情况
package example.juc.test2;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestLock { public static void main(String[] args) { TicketTask ticketTask = new TicketTask(); new Thread(() -> { for (int i = 0; i < 1000; i++) { ticketTask.ticket(); } }, "线程A").start(); new Thread(() -> { for (int i = 0; i < 1000; i++) { ticketTask.ticket(); } }, "线程B").start(); new Thread(() -> { for (int i = 0; i < 1000; i++) { ticketTask.ticket(); } }, "线程C").start(); }}class TicketTask { private int number = 30; private Lock lock = new ReentrantLock(); public void ticket() { lock.lock(); try { if (number > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } number--; System.out.println(Thread.currentThread().getName() + "完成售票:" + number); } } finally { lock.unlock(); } }}出现这种情况的原因就在于,ReentrantLock默认是非公平的
java.util.concurrent.locks.ReentrantLock
public ReentrantLock() { sync = new NonfairSync();}......public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}如果想要实现公平的效果,创建对象时需要加上一个初始化参数
private Lock lock = new ReentrantLock(true);公平锁的优点在于“公平”,缺点在于效率较低
4.可重入
可重入锁指的是,一个线程在一个方法外层获取了锁 ,进入方法内层会自动获取锁。
和synchronized一样,ReentrantLock也是可重入的,但是需要注意加锁和释放次数要匹配,否则其他线程无法获得锁,例如:
package example.juc3;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestReentrantLock { public static void main(String[] args) { Lock lock = new ReentrantLock(); new Thread(() -> { lock.lock(); try { System.out.println("外层"); lock.lock(); try { System.out.println("内层"); } finally { lock.unlock(); } } finally { lock.unlock(); } },"t1").start(); new Thread(() -> { lock.lock(); try { System.out.println("2"); } finally { lock.unlock(); } },"t2").start(); }}5.响应中断
synchronized是无法支持响应中断的,一个线程获取不到锁,就会一直等着,程序无法结束,造成死锁。
而ReentrantLock支持通过tryLock()设置一个时间参数,到时间后自动放弃获取锁,如果不设置时间参数代表立即返回获取锁的结果,通过这个可以避免死锁现象
例1:派发1000个线程,每个为变量加1,结果抢不到的直接放弃,所以变量被加的远远不到1000
package example.juc2.test;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestTryLock { private static final Lock lock = new ReentrantLock(); private static int sum = 0; public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { if (lock.tryLock()) { try { sum ++; System.out.println(Thread.currentThread().getName() +" - "+ sum); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }).start(); } }}例2:每个抢到锁的线程1秒钟才能执行完成,每个抢不到锁的线程最多等待5秒否则直接放弃争抢锁,因此程序只能打印5次左右
package example.juc2.test;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestTryLock { private static final Lock lock = new ReentrantLock(); private static int sum = 0; public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { try { sum ++; System.out.println(Thread.currentThread().getName() +" - "+ sum); TimeUnit.SECONDS.sleep(1); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } catch (InterruptedException e) { throw new RuntimeException(e); } }).start(); } }}6.精准唤醒
如果说Lock代替了synchronized的使用,Condition则是替代了Object的wait(),notify(),notifyAll()方法,java.util.concurrent.locks.Condition是一个接口,通过调用Lock对象的newCondition()方法获取具体Condition对象,将Condition绑定在Lock上,通过Condition的await(),signal(),signalAll()实现线程之间的通信,与传统Object作为同步监视器一样,Condition的await()也要总是出现在循环中,实现二次条件判断。
例:实现一个资源类AirConditioner,然后新建4个线程,每个线程对AirConditioner中的变量number交替改成0和1
package example.juc2.test;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestConditionWaitNotify { public static class AirConditioner { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() { lock.lock(); try { while (number != 0) { condition.await(); } number ++; System.out.println(Thread.currentThread().getName()+" 修改为 "+number); condition.signalAll(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void decrement() { lock.lock(); try { while (number == 0) { condition.await(); } number --; System.out.println(Thread.currentThread().getName()+" 修改为 "+number); condition.signalAll(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } public static void main(String[] args) { AirConditioner airConditioner = new AirConditioner(); new Thread(() -> { for (int i=0;i<10;i++) { airConditioner.increment(); } }, "T1").start(); new Thread(() -> { for (int i=0;i<10;i++) { airConditioner.decrement(); } }, "T2").start(); new Thread(() -> { for (int i=0;i<10;i++) { airConditioner.increment(); } }, "T3").start(); new Thread(() -> { for (int i=0;i<10;i++) { airConditioner.decrement(); } }, "T4").start(); }}运行结果:
T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T3 修改为 1T2 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T3 修改为 1T4 修改为 0T1 修改为 1T4 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0T1 修改为 1T2 修改为 0上面的例子中,线程对资源类的操作是无序的,和加了synchronized的效果一样,而Condition有一个比synchronized更强大的地方,那就是能精确的控制等待唤醒。如果希望几个线程有序的交替执行,就可以获取多个监视器Condition绑定同一个Lock,实现精确的定制化通信。
例:三个线程ABC,按A-B-C顺序执行资源类ShareResource中的打印任务,并循环交替3轮(ABC-ABC-ABC),A执行时打印5次,B10次,C15次,通过lock.newCondition()为三个线程分别创建3个同步监视器:conditionA,conditionB, conditionC,再通过标志位变量flag判断当前该谁执行了,开始默认为A。
程序执行,A线程先获得执行权,执行第一轮,执行完成唤醒B,执行第一轮,B线程执行完成唤醒C,C也执行第一轮,完成再唤醒A,A开始执行第二轮,以此类推直到三轮任务全部完成,实现每一轮都是按照A-B-C的顺序执行。。
package example.juc2.test;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class TestThreadOrderAccess { public static class ShareResource { private char flag = 'A'; private final Lock lock = new ReentrantLock(); private final Condition conditionA = lock.newCondition(); private final Condition conditionB = lock.newCondition(); private final Condition conditionC = lock.newCondition(); public void doA() { lock.lock(); try { while (flag != 'A') { conditionA.await(); } for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "\t" +i); } flag = 'B'; conditionB.signal(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void doB() { lock.lock(); try { while (flag != 'B') { conditionB.await(); } for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "\t" +i); } flag = 'C'; conditionC.signal(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void doC() { lock.lock(); try { while (flag != 'C') { conditionC.await(); } for (int i = 0; i < 15; i++) { System.out.println(Thread.currentThread().getName() + "\t" +i); } System.out.println("*********"); flag = 'A'; conditionA.signal(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } public static void main(String[] args) { ShareResource shareResource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 3; i++) { shareResource.doA(); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 3; i++) { shareResource.doB(); } }, "B").start(); new Thread(() -> { for (int i = 0; i < 3; i++) { shareResource.doC(); } }, "C").start(); }}最终运行结果:
A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********A0A1A2A3A4B0B1B2B3B4B5B6B7B8B9C0C1C2C3C4C5C6C7C8C9C10C11C12C13C14*********7.总结
Lock和synchronized都是独占锁,可重入锁,但是synchronized获取/释放锁是JVM自动完成,Lock是需要开发者手动完成。synchronized不能响应中断,Lock可以响应中断,synchronized无法精准唤醒,Lock可以实现精准唤醒。