java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)
目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性、可见性、有序性
对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方法默认是当前对象this,对于同步代码块,需要指定锁对象
对于整个同步方法或者代码块,不再需要显式的进行加锁,默认这一整个范围都是在锁范围内
可以理解为,隐含的在代码开始和结尾处,进行了隐式的加锁和解锁
所以synchronized又被称为隐式锁
对于synchronized关键字的隐式锁,不需要显式的加锁和释放,即使出现了问题,仍旧能够对锁进行释放
synchronized是一种阻塞式的,在前面也提到过,对于synchronized修饰的同步,如果无法进入监视器则是BLOCKED状态,无疑,性能方面可想而知
而且,这种隐式锁,在同一个代码片段内只有一个监视器,灵活性不够
为了优化synchronized的一些不便,Java又提出来了显式锁的概念Lock
顾名思义,显式,是相对隐式来说的,也就是对于加锁和解锁,需要明确的给出,而不会自动的进行处理
示例回顾
回忆下是之前《多线程协作wait、notify、notifyAll方法简介理解使用 》一文中使用的例子
ps:下面的例子是优化过的,其中if判断换成了while 循环检测,notify换成了notifyAll
package test1; import java.util.LinkedList; /** * 消息队列MessageQueue 测试 */ public class T14 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //创建生产者线程并启动 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer"+i).start(); } //创建消费者线程并启动 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } } /** * 消息队列 */ class RefactorMessageQueue { /** * 队列最大值 */ private final int max; /* * 锁 * */ private final byte[] lock = new byte[1]; /** * final确保发布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 构造函数默认队列大小为10 */ public RefactorMessageQueue() { max = 10; } /** * 构造函数设置队列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { synchronized (lock) { //如果已经大于队列个数,队列满,进入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果队列未满,生产消息,随后通知lock上的等待线程 //每一次的消息生产,都会通知消费者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); lock.notifyAll(); } } public void get() { synchronized (lock) { //如果队列为空,进入等待,无法获取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列非空时,读取消息,随后通知lock上的等待线程 //每一次的消息读取,都会通知生产者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); lock.notifyAll(); } } }
分析下这个示例中的一些概念
使用了synchronized用作同步,锁对象为 private final byte[] lock = new byte[1];
有多个生产者和一个消费者,为了进行通信使用了监视器(也就是锁对象)的wait和notifyAll方法进行通信
ps:前文也说过为何要用notifyAll而不是notify
简单说两个点:
- synchronized关键字
- 监视器方法
借助于这两个点,可以完成多线程之间的协作与通信(多个生产者一个消费者)
监视器方法的调用需要在监视器内,也就是同步方法内
而且上面的例子中的监视器都是同一个就是锁对象,wait是当前线程在监视器上wait,notifyAll方法则是唤醒所有在此监视器上等待的线程
很显然,其实生产者应该唤醒生产者,消费者应该唤醒消费者
可是,多线程协作使用的是同一个队列,所以需要使用同一把锁
又因为监视器方法必须在同步方法内而且也必须是持有监视器才能调用相应的监视器方法,所以只能使用同一个监视器了
也就是只能将这些线程组织在同一个监视器中,就不好做到“其实生产者应该唤醒生产者,消费者应该唤醒消费者”
显式锁逻辑
再回过头看显式锁,他是如何做到各方面灵活的呢?
从上面的分析来看主要就是因为隐式锁与监视器之间的比较强的关联关系
synchronized修饰的代码片段使用的是同一把锁,同步方法内的监视器方法也只能调用这个锁的,也就是说在使用上来看,用什么锁,就要用这个锁的监视器,强关联
问题的一种解题思路就是解耦,显式锁就是这种思路
Lock就好比是synchronized关键字,只不过你需要显式的进行加锁和解锁
惯用套路如下
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
本来使用synchronized隐式的加锁和解锁,换成了Lock的lock和unlock方法调用
那么监视器呢?
与锁关联的监视器又是什么,又如何调用监视器的方法呢?
Lock提供了Condition newCondition();方法
返回类型为Condition,被称之为条件变量,可以认为是锁关联的监视器
借助于Condition,就可以达到原来监视器方法调用的效果,Condition方法列表如下,看得出来,是不是很像wait和notify、notifyAll?目标是一致的
所以可以说,显式锁的逻辑就是借助于Lock接口以及Condition接口,实现了对synchronized关键字以及锁对应的监视器的另外的一种实现
从而提供了更大的灵活性
还是之前的示例,尝试试用一下显式锁
package test2; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class T26 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //创建生产者线程并启动 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer" + i).start(); } //创建消费者线程并启动 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } /** * 消息队列中存储的消息 */ static class Message { } /** * 消息队列 */ static class RefactorMessageQueue { /** * 队列最大值 */ private final int max; /* * 锁 * */ private final Lock lock = new ReentrantLock(); /** * 条件变量 */ private final Condition condition = lock.newCondition(); /** * final确保发布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 构造函数默认队列大小为10 */ public RefactorMessageQueue() { max = 10; } /** * 构造函数设置队列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { lock.lock(); try { //如果已经大于队列个数,队列满,进入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果队列未满,生产消息,随后通知lock上的等待线程 //每一次的消息生产,都会通知消费者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); condition.signalAll(); } finally { } lock.unlock(); } public void get() { lock.lock(); try { //如果队列为空,进入等待,无法获取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列非空时,读取消息,随后通知lock上的等待线程 //每一次的消息读取,都会通知生产者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); condition.signalAll(); } finally { lock.unlock(); } } } }
改变的核心逻辑就是锁和条件变量
/* * 锁 * */ private final Lock lock = new ReentrantLock(); /** * 条件变量 */ private final Condition condition = lock.newCondition();
- 使用lock.lock();以及lock.unlock(); 替代了synchronized(lock)
- 使用condition的await和signalAll方法替代了lock.wait()和 lock.notifyAll
看起来与使用synchronized关键字好像差不多,这没什么毛病
显式锁的设计本来就是为了弥补隐式锁的,虽说不是说作为一种替代品,但是功能逻辑的相似性是必然的
注意到,使用条件变量,与隐式锁中都是只有一个监视器,所有的线程仍旧都是被唤醒
前面提到过,其实生产者应该唤醒消费者,消费者才应该唤醒生产者
是不是可以两个变量?
对于生产者来说,只要非满即可,如果满了等待,非满生产然后唤醒消费者
对于消费者来说,只要非空即可,如果空了等待,非空消费然后唤醒生产者
可以定义两个条件变量,如下所示完整代码
其实只是定义了两个监视器
package test2; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class T27 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //创建生产者线程并启动 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer" + i).start(); } //创建消费者线程并启动 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } /** * 消息队列中存储的消息 */ static class Message { } /** * 消息队列 */ static class RefactorMessageQueue { /** * 队列最大值 */ private final int max; /* * 锁 * */ private final Lock lock = new ReentrantLock(); /** * 条件变量,用于消费者,非空即可消费 */ private final Condition notEmptyCondition = lock.newCondition(); /** * 条件变量,用于生产者,非满即可生产 */ private final Condition notFullCondition = lock.newCondition(); /** * final确保发布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 构造函数默认队列大小为10 */ public RefactorMessageQueue() { max = 10; } /** * 构造函数设置队列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { lock.lock(); try { //如果已经大于队列个数,队列满,进入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); //如果满了,生产者在“非满”这个条件上等待 notFullCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果队列未满,生产消息,随后通知lock上的等待线程 //每一次的消息生产,都会通知消费者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); //生产后,增加了消息,非空条件满足,需要唤醒消费者 notEmptyCondition.signalAll(); } finally { } lock.unlock(); } public void get() { lock.lock(); try { //如果队列为空,进入等待,无法获取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); //如果空了,消费者需要在“非空”条件上等待 notEmptyCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列非空时,读取消息,随后通知lock上的等待线程 //每一次的消息读取,都会通知生产者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); //消费后,减少了消息,所以非满条件满足,需要唤醒生产者 notFullCondition.signalAll(); } finally { lock.unlock(); } } } }
总结
通过上面的示例,应该可以理解显式锁的思路
他与隐式锁并没有像名称上看起来这么对立(一个隐 一个显),他们的核心仍旧是为了解决线程的同步与线程间的通信协作
线程同步与通信的在Java中的底层核心概念为锁和监视器
不管是synchronized还是Lock,不管是Object提供的通信方法还是Condition中的方法,都还是围绕着锁和监视器的概念展开的
如同平时写代码,同样的功能,可能会有多种实现方式,显式锁和隐式锁也是类似的,他们的实现有着很多的不同,也都有各种利弊
所以才会有隐式锁和显式锁,在程序中很难找到“放之四海而皆准”的实现代码,所以才会有各种各样的解决方案
尽管早期synchronized关键字性能比较低,但是随着版本的升级,性能也有了很大的改善
所以官方也是建议如果场景满足,还是尽可能使用synchronized关键字而不是显式锁
显式锁是为了解决隐式锁而不好解决的一些场景而存在的,尽管本文并没有体现出来他们之间的差异(本文恰恰相反,对相同点进行了介绍)
但是显式锁有很多隐式锁不存在的优点,后续慢慢介绍,通过本文希望理解,显式锁也只是线程同步与协作通信的一种实现途径而已