目前对于同步,仅仅介绍了一个关键字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关键字而不是显式锁
显式锁是为了解决隐式锁而不好解决的一些场景而存在的,尽管本文并没有体现出来他们之间的差异(本文恰恰相反,对相同点进行了介绍)
但是显式锁有很多隐式锁不存在的优点,后续慢慢介绍,通过本文希望理解,显式锁也只是线程同步与协作通信的一种实现途径而已

java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)的更多相关文章

  1. Java并发编程(三)概念介绍

    在构建稳健的并发程序时,必须正确使用线程和锁.但是这终归只是一些机制.要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问. 对 ...

  2. java并发里的一些基础概念

    转载自:https://my.oschina.net/hosee/blog/597934: 摘要: 本系列基于炼数成金课程,为了更好的学习,做了系列的记录. 本文主要介绍 1.高并发的概念,为以后系列 ...

  3. java并发系列(四)-----源码角度彻底理解ReentrantLock(重入锁)

    1.前言 ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值,继续跟进下源码我们就能发现,关键在于实例化内部变量sync的方式不同,如下所示: /** * ...

  4. Java并发基础类AbstractQueuedSynchronizer的实现原理简介

    1.引子 Lock接口的主要实现类ReentrantLock 内部主要是利用一个Sync类型的成员变量sync来委托Lock锁接口的实现,而Sync继承于AbstractQueuedSynchroni ...

  5. Java并发编程原理与实战三十九:JDK8新增锁StampedLock详解

    1.StampedLock是做什么的? ----->它是ReentrantReadWriteLock 的增强版,是为了解决ReentrantReadWriteLock的一些不足.   2.Ree ...

  6. Java并发编程原理与实战十七:AQS实现重入锁

    一.什么是重入锁 可重入锁就是当前持有锁的线程能够多次获取该锁,无需等待 二.什么是AQS AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性可 ...

  7. Java并发编程——线程的基本概念和创建

    一.线程的基本概念: 1.什么是进程.什么是是线程.多线程? 进程:一个正在运行的程序(程序进入内存运行就变成了一个进程).比如QQ程序就是一个进程. 线程:线程是进程中的一个执行单元,负责当前进程中 ...

  8. java并发编程基础 --- 4.1线程简介

    一.线程简介 什么是线程: 现在操作系统在运行一个程序时,会为其创建一个进程.例如,启动一个java程序,操作系统就会创建一个java进程.现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程 ...

  9. 并发系列2:Java并发的基石,volatile关键字、synchronized关键字、乐观锁CAS操作

    由并发大师Doug Lea操刀的并发包Concurrent是并发编程的重要包,而并发包的基石又是volatile关键字.synchronized关键字.乐观锁CAS操作这些基础.因此了解他们的原理对我 ...

随机推荐

  1. bzoj 4129 Haruna’s Breakfast 树上莫队

    按照dfs序分块,莫队乱搞 再套个权值分块 #include<cstdio> #include<iostream> #include<cstring> #inclu ...

  2. bzoj 2500 幸福的道路 树上直径+set

    首先明确:树上任意一点的最长路径一定是直径的某一端点. 所以先找出直径,求出最长路径,然后再求波动值<=m的最长区间 #include<cstdio> #include<cst ...

  3. 【游戏开发】在Lua中实现面向对象特性——模拟类、继承、多态

    一.简介 Lua是一门非常强大.非常灵活的脚本语言,自它从发明以来,无数的游戏使用了Lua作为开发语言.但是作为一款脚本语言,Lua也有着自己的不足,那就是它本身并没有提供面向对象的特性,而游戏开发是 ...

  4. POI读取excel文件。

    1) poi读取现成.xls文件,不需要自己建立.xls ====ReadExcel​类​==== package cust.com.excelToDataTest; import java.io.F ...

  5. Python爬虫入门教程 64-100 反爬教科书级别的网站-汽车之家,字体反爬之二

    说说这个网站 汽车之家,反爬神一般的存在,字体反爬的鼻祖网站,这个网站的开发团队,一定擅长前端吧,2019年4月19日开始写这篇博客,不保证这个代码可以存活到月底,希望后来爬虫coder,继续和汽车之 ...

  6. RabbitMQ和Kafka到底怎么选(二)?

    前言 前一篇文章<RabbitMQ和Kafka到底怎么选?>,我们在吞吐量方面比较了Kafka和RabbitMQ,知道了Kafka的吞吐量要高于RabbitMQ.本文从可靠性方面继续探讨两 ...

  7. 企业私有源代码上传github致入侵之大疆案判决了

    事件简单回顾: 1.2017年8月28日,大疆宣布“大疆威胁识别奖励计划”,最高3万美元: 2.然而在此之前,大疆农业事业部某员工将企业私有源代码上传到了github: 3.就职于大疆竞对公司Depa ...

  8. object类和内部类

    1.Object object类是所有类的根类(父类). 如果一个类没有显示继承另外一个类,那么该类一定继承于object类. toString()返回对象字符串的的形式. public class ...

  9. "元素隐式具有 “any” 类型,因为类型“Shared”没有索引签名"问题解决思路

    最近在构建一个typescript项目时如下代码在项目框架里vscode报错元素隐式具有 "any" 类型,因为类型“Shared”没有索引签名;很有意思的是当我们单独的把这段代码 ...

  10. go语言调度器源代码情景分析之五:汇编指令

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第4小节. 汇编语言是每位后端程序员都应该掌握的一门语言,因为学会了汇编语言,不管是对我们调试程序还是研究与理解计算机底层的一些运 ...