显示锁是什么? 显式锁就是程序员通过 Lock 接口手动 lock() 加锁、手动 unlock() 解锁的锁。它和 synchronized 这种“隐式锁”相对
Lock 和 Condition 为何引入 Lock 和 Condition 并发编程领域两大核心问题:互斥 和同步 。这两大问题,管程(Monitor)都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题 。
管程是一个理论模型,而synchronized编译之后的monitor就是这个理论的实现。这里的Lock和Condition也是。
既然 synchronized 是管程的一种实现,何必再提供 Lock 和 Condition。
JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。但是,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会出现死锁。
synchronized 无法通过破坏不可抢占条件 来避免死锁。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
与内置锁 synchronized 不同的是,Lock 提供了一组无条件的、可轮询的、定时的以及可中断的锁操作 ,所有获取锁、释放锁的操作都是显式的操作。
能够响应中断 。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
支持超时 。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
非阻塞地获取锁 。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
Lock 接口 Lock 的接口定义如下:
1 2 3 4 5 6 7 8 public interface Lock { void lock () ; void lockInterruptibly () throws InterruptedException; boolean tryLock () ; boolean tryLock (long time, TimeUnit unit) throws InterruptedException; void unlock () ; Condition newCondition () ; }
lock() - 获取锁。
unlock() - 释放锁。
tryLock() - 尝试获取锁,仅在调用时锁未被另一个线程持有的情况下,才获取该锁。
tryLock(long time, TimeUnit unit) - 和 tryLock() 类似,区别仅在于限定时间,如果限定时间内未获取到锁,视为失败。
lockInterruptibly() - 锁未被另一个线程持有,且线程没有被中断的情况下,才能获取锁。
newCondition() - 返回一个绑定到 Lock 对象上的 Condition 实例。
Condition Condition 实现了管程模型里面的条件变量 。
在单线程中,一段代码的执行可能依赖于某个状态,如果不满足状态条件,代码就不会被执行(典型的场景,如:if ... else ...)。在并发环境中,当一个线程判断某个状态条件时,其状态可能是由于其他线程的操作而改变,这时就需要有一定的协调机制来确保在同一时刻,数据只能被一个线程锁修改,且修改的数据状态被所有线程所感知。
Java 1.5 之前,主要是利用 Object 类中的 wait、notify、notifyAll 配合 synchronized 来进行线程间通信。
wait、notify、notifyAll 需要配合 synchronized 使用,不适用于 Lock。而使用 Lock 的线程,彼此间通信应该使用 Condition 。这可以理解为,什么样的锁配什么样的钥匙。内置锁(synchronized)配合内置条件队列(wait、notify、notifyAll ),显式锁(Lock)配合显式条件队列(Condition ) 。
Condition 的特性 Condition 接口定义如下:
1 2 3 4 5 6 7 8 9 public interface Condition { void await () throws InterruptedException; void awaitUninterruptibly () ; long awaitNanos (long nanosTimeout) throws InterruptedException; boolean await (long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil (Date deadline) throws InterruptedException; void signal () ; void signalAll () ; }
其中,await、signal、signalAll 与 wait、notify、notifyAll 相对应,功能也相似。除此以外,Condition 相比内置条件队列( wait、notify、notifyAll ),提供了更为丰富的功能:
每个锁(Lock)上可以存在多个 Condition,这意味着锁的状态条件可以有多个。
支持公平的或非公平的队列操作。
支持可中断的条件等待,相关方法:awaitUninterruptibly() 。
支持可定时的等待,相关方法:awaitNanos(long) 、await(long, TimeUnit)、awaitUntil(Date)。
Condition 的用法 这里以 Condition 来实现一个消费者、生产者模式。
产品类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class Message { private final Lock lock = new ReentrantLock (); private final Condition producedMsg = lock.newCondition(); private final Condition consumedMsg = lock.newCondition(); private String message; private boolean state; private boolean end; public void consume () { lock.lock(); try { while (!state) { producedMsg.await(); } System.out.println("consume message : " + message); state = false ; consumedMsg.signal(); } catch (InterruptedException ie) { System.out.println("Thread interrupted - viewMessage" ); } finally { lock.unlock(); } } public void produce (String message) { lock.lock(); try { while (state) { consumedMsg.await(); } System.out.println("produce msg: " + message); this .message = message; state = true ; producedMsg.signal(); } catch (InterruptedException ie) { System.out.println("Thread interrupted - publishMessage" ); } finally { lock.unlock(); } } public boolean isEnd () { return end; } public void setEnd (boolean end) { this .end = end; } }
消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MessageConsumer implements Runnable { private Message message; public MessageConsumer (Message msg) { message = msg; } @Override public void run () { while (!message.isEnd()) { message.consume(); } } }
生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class MessageProducer implements Runnable { private Message message; public MessageProducer (Message msg) { message = msg; } @Override public void run () { produce(); } public void produce () { List<String> msgs = new ArrayList <>(); msgs.add("Begin" ); msgs.add("Msg1" ); msgs.add("Msg2" ); for (String msg : msgs) { message.produce(msg); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } message.produce("End" ); message.setEnd(true ); } }
测试
1 2 3 4 5 6 7 8 9 10 public class LockConditionDemo { public static void main (String[] args) { Message msg = new Message (); Thread producer = new Thread (new MessageProducer (msg)); Thread consumer = new Thread (new MessageConsumer (msg)); producer.start(); consumer.start(); } }
ReentrantLock ReentrantLock 类是 Lock 接口的具体实现,与内置锁 synchronized 相同的是,它是一个可重入锁 。
ReentrantLock 的特性 ReentrantLock 的特性如下:
ReentrantLock 提供了与 synchronized 相同的互斥性、内存可见性和可重入性 。
ReentrantLock 支持公平锁和非公平锁 (默认)两种模式。
ReentrantLock 实现了 Lock 接口,支持了 synchronized 所不具备的灵活性 。
synchronized 无法中断一个正在等待获取锁的线程
synchronized 无法在请求获取一个锁时无休止地等待
ReentrantLock 的用法 前文了解了 ReentrantLock 的特性,接下来,我们要讲述其具体用法。
ReentrantLock 的构造方法 ReentrantLock 有两个构造方法:
1 2 public ReentrantLock () {}public ReentrantLock (boolean fair) {}
ReentrantLock() - 默认构造方法会初始化一个非公平锁(NonfairSync) ;
ReentrantLock(boolean) - new ReentrantLock(true) 会初始化一个公平锁(FairSync) 。
lock 和 unlock 方法
lock() - 无条件获取锁 。如果当前线程无法获取锁,则当前线程进入休眠状态不可用,直至当前线程获取到锁。如果该锁没有被另一个线程持有,则获取该锁并立即返回,将锁的持有计数设置为 1。
unlock() - 用于释放锁 。
🔔 注意:请务必牢记,获取锁操作 lock() 必须在 try catch 块中进行,并且将释放锁操作 unlock() 放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生 。
示例:ReentrantLock 的基本操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class ReentrantLockDemo { public static void main (String[] args) { Task task = new Task (); MyThread tA = new MyThread ("Thread-A" , task); MyThread tB = new MyThread ("Thread-B" , task); MyThread tC = new MyThread ("Thread-C" , task); tA.start(); tB.start(); tC.start(); } static class MyThread extends Thread { private Task task; public MyThread (String name, Task task) { super (name); this .task = task; } @Override public void run () { task.execute(); } } static class Task { private ReentrantLock lock = new ReentrantLock (); public void execute () { lock.lock(); try { for (int i = 0 ; i < 3 ; i++) { System.out.println(lock.toString()); System.out.println("\t holdCount: " + lock.getHoldCount()); System.out.println("\t queuedLength: " + lock.getQueueLength()); System.out.println("\t isFair: " + lock.isFair()); System.out.println("\t isLocked: " + lock.isLocked()); System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread()); try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } } } finally { lock.unlock(); } } } }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A] holdCount: 1 queuedLength: 2 isFair: false isLocked: true isHeldByCurrentThread: true java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C] holdCount: 1 queuedLength: 1 isFair: false isLocked: true isHeldByCurrentThread: true
tryLock 方法 与无条件获取锁相比,tryLock 有更完善的容错机制。
tryLock() - 可轮询获取锁 。如果成功,则返回 true;如果失败,则返回 false。也就是说,这个方法无论成败都会立即返回 ,获取不到锁(锁已被其他线程获取)时不会一直等待。
tryLock(long, TimeUnit) - 可定时获取锁 。和 tryLock() 类似,区别仅在于这个方法在获取不到锁时会等待一定的时间 ,在时间期限之内如果还获取不到锁,就返回 false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。
示例:ReentrantLock 的 tryLock() 操作
修改上个示例中的 execute() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public void execute () { if (lock.tryLock()) { try { for (int i = 0 ; i < 3 ; i++) { } } finally { lock.unlock(); } } else { System.out.println(Thread.currentThread().getName() + " 获取锁失败" ); } }
示例:ReentrantLock 的 tryLock(long, TimeUnit) 操作
修改上个示例中的 execute() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void execute () { try { if (lock.tryLock(2 , TimeUnit.SECONDS)) { try { for (int i = 0 ; i < 3 ; i++) { } } finally { lock.unlock(); } } else { System.out.println(Thread.currentThread().getName() + " 获取锁失败" ); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 获取锁超时" ); e.printStackTrace(); } }
lockInterruptibly 方法
lockInterruptibly() - 可中断获取锁 。可中断获取锁可以在获得锁的同时保持对中断的响应。可中断获取锁比其它获取锁的方式稍微复杂一些,需要两个 try-catch 块(如果在获取锁的操作中抛出了 InterruptedException ,那么可以使用标准的 try-finally 加锁模式)。
举例来说:假设有两个线程同时通过 lock.lockInterruptibly() 获取某个锁时,若线程 A 获取到了锁,则线程 B 只能等待。若此时对线程 B 调用 threadB.interrupt() 方法能够中断线程 B 的等待过程。由于 lockInterruptibly() 的声明中抛出了异常,所以 lock.lockInterruptibly() 必须放在 try 块中或者在调用 lockInterruptibly() 的方法外声明抛出 InterruptedException。
注意:当一个线程获取了锁之后,是不会被 interrupt() 方法中断的。单独调用 interrupt() 方法不能中断正在运行状态中的线程,只能中断阻塞状态中的线程。因此当通过 lockInterruptibly() 方法获取某个锁时,如果未获取到锁,只有在等待的状态下,才可以响应中断。
示例:ReentrantLock 的 lockInterruptibly() 操作
修改上个示例中的 execute() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void execute () { try { lock.lockInterruptibly(); for (int i = 0 ; i < 3 ; i++) { } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中断" ); e.printStackTrace(); } finally { lock.unlock(); } }
newCondition 方法 newCondition() - 返回一个绑定到 Lock 对象上的 Condition 实例。Condition 的特性和具体方法请阅读🚀Condition 。
ReentrantLock 的原理 ReentrantLock 通过内存屏障和 happens-before 关系来保证可见性。
happens-before 是 Java 内存模型(JMM)中的核心概念,它定义了操作之间的内存可见性保证,是理解 Java 并发编程的基础。
什么是 Happens-Before? Happens-Before 关系表示:如果操作 A happens-before 操作 B,那么:
A 操作的结果对 B 操作可见
A 操作按顺序排在 B 操作之前
🔔注意:这并不一定意味着在时间上 A 真的在 B 之前执行,而是指从内存可见性的角度来看,A 的效果对 B 是可见的。
ReentrantReadWriteLock 当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。 类似于数据库中的 select ...from ... lock in share mode
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的write() 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class DataContainer { private Object data; private ReentrantReadWriteLock rw = new ReentrantReadWriteLock (); private ReentrantReadWriteLock.ReadLock r = rw.readLock(); private ReentrantReadWriteLock.WriteLock w = rw.writeLock(); public Object read () { log.debug("获取读锁..." ); r.lock(); try { log.debug("读取" ); sleep(1 ); return data; } finally { log.debug("释放读锁..." ); r.unlock(); } } public void write () { log.debug("获取写锁..." ); w.lock(); try { log.debug("写入" ); sleep(1 ); } finally { log.debug("释放写锁..." ); w.unlock(); } } }
测试 读锁-读锁 可以并发
1 2 3 4 5 6 7 8 DataContainer dataContainer = new DataContainer ();new Thread (() -> { dataContainer.read(); }, "t1" ).start(); new Thread (() -> { dataContainer.read(); }, "t2" ).start();
输出结果,从这里可以看到 t2 锁定期间,t1 的读操作不受影响
1 2 3 4 5 6 14:05:14.341 c.DataContainer [t2] - 获取读锁... 14:05:14.341 c.DataContainer [t1] - 获取读锁... 14:05:14.345 c.DataContainer [t1] - 读取 14:05:14.345 c.DataContainer [t2] - 读取 14:05:15.365 c.DataContainer [t2] - 释放读锁... 14:05:15.386 c.DataContainer [t1] - 释放读锁..
测试 读锁-写锁 相互阻塞
1 2 3 4 5 6 7 8 9 DataContainer dataContainer = new DataContainer ();new Thread (() -> { dataContainer.read(); }, "t1" ).start(); Thread.sleep(100 ); new Thread (() -> { dataContainer.write(); }, "t2" ).start();
输出结果
1 2 3 4 5 6 14:04:21.838 c.DataContainer [t1] - 获取读锁... 14:04:21.838 c.DataContainer [t2] - 获取写锁... 14:04:21.841 c.DataContainer [t2] - 写入 14:04:22.843 c.DataContainer [t2] - 释放写锁... 14:04:22.843 c.DataContainer [t1] - 读取 14:04:23.843 c.DataContainer [t1] - 释放读锁...
写锁-写锁 也是相互阻塞的
🔔注意:
读锁不支持条件变量
重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
重入时降级支持:即持有写锁的情况下去获取读锁