目前对于同步,仅仅介绍了一个关键字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. 【递推】Bzoj3612[Heoi2014]平衡

    Description 下课了,露露.花花和萱萱在课桌上用正三棱柱教具和尺子摆起了一个“跷跷板”.      这个“跷跷板”的结构是这样的:底部是一个侧面平行于地平面的正三棱柱教具, 上面 摆着一个尺 ...

  2. BZOJ_2599_[IOI2011]Race_点分治

    BZOJ_2599_[IOI2011]Race_点分治 Description 给一棵树,每条边有权.求一条简单路径,权值和等于K,且边的数量最小.N <= 200000, K <= 10 ...

  3. BZOJ1500 [NOI2005]维修数列-fhq_Treap

    题面见这里 反正是道平衡树,就拿 fhq_Treap 写了写... 这道题思路基本是围绕“用 Treap 维护中序遍历” 和 中序遍历的性质 来进行的操作 所以就可以类比线段树进行一些操作 1. 建树 ...

  4. 由ORACLE:ORA-28001: the password has expired(密码过期)引起ODI资料库连接失败

    今天,连接ODI,出现下面的错误 oracle.odi.core.config.WorkRepositoryResourceFailureException: ODI-10182: 资料档案库访问期间 ...

  5. python接口自动化(二十二)--unittest执行顺序隐藏的坑(详解)

    简介 大多数的初学者在使用 unittest 框架时候,不清楚用例的执行顺序到底是怎样的.对测试类里面的类和方法分不清楚,不知道什么时候执行,什么时候不执行.虽然或许通过代码实现了,也是稀里糊涂的一知 ...

  6. Android6.0 源码修改之 Contacts应用

    一.Contacts应用的主界面和联系人详情界面增加顶部菜单添加退出按钮 通过Hierarchy View 工具可以发现 主界面对应的类为 PeopleActivity 联系人详情界面对应的类为 Qu ...

  7. len(x) 击败 x.len(),从内置函数看 Python 的设计思想

    内置函数是 Python 的一大特色,用极简的语法实现很多常用的操作. 它们预先定义在内置命名空间中,开箱即用,所见即所得.Python 被公认是一种新手友好型的语言,这种说法能够成立,内置函数在其中 ...

  8. 马蜂窝搜索基于 Golang 并发代理的一次架构升级

    搜索业务是马蜂窝流量分发的重要入口.很多用户在使用马蜂窝时,都会有目的性地主动搜索与自己旅行需求相关的各种信息,衣食住行,事无巨细,从而做出最符合需求的旅行决策. 因此在马蜂窝,搜索业务交互的下游模块 ...

  9. 【Java】几道常见的秋招面试题

    前言 只有光头才能变强 Redis目前还在看,今天来分享一下我在秋招看过(遇到)的一些面试题(相对比较常见的) 0.final关键字 简要说一下final关键字,final可以用来修饰什么? 这题我是 ...

  10. BFPRT算法

    解决的问题:在一个数组中找到最小的k个数 常规解法:1.排序,输出前k个数,时间复杂度O(n*log(n)). 2.利用一个大小为k的大根堆,遍历数组维持大根堆,最后返回大根堆就可以了,时间复杂度O( ...