【Java并发系列】----JUC之Lock
显式锁 Lock
在Java 5.0之前,协调共享对象的访问时可以使用的机制只有synchronized和volatile。Java 5.0后增加了一些新的机制,但并不是一种替代内置锁的方法,而是当内置锁不适用时,作为一种可选择的高级功能。
ReentrantLock(可重入锁) 实现了 Lock 接口,并提供了与synchronized 相同的互斥性和内存可见性。但相较于synchronized 提供了更高的处理锁的灵活性 。
部分内容引用自 并发编程网:
一个简单的Lock示例:
public class Counter{
private Lock lock = new Lock();
private int count = 0; public int inc(){
lock.lock();
int newCount = ++count;
lock.unlock();
return newCount;
}
}与synchronized关键字给当前类对象上锁不同,lock方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。
下面是一个Lock类的简单实现(非源码):
public class Counter{
public class Lock{
private boolean isLocked = false; public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
} public synchronized void unlock(){
isLocked = false;
notify();
}
}注意其中的while(isLocked)循环,它又被叫做“自旋锁”。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。
当线程完成了临界区(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且通知(唤醒)其中一个(若有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。
这里之所以使用while循环而不是用if判断唤醒条件,是为防止线程的 虚假唤醒。同时在JDK说明文档中也建议在监视唤醒时使用循环:
锁的可重入性
概念:如果一个线程已经拥有了一个管程(监视器)对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
例如在synchronized关键字中,如果两个方法或代码块所同步的是同一个对象,那么在这两个方法或代码块相互调用或运行时,是不会被这个synchronized同步所阻塞的。
在前面的Lock简单实现例子中,是无法实现线程重入的,如下例所示:
public class Reentrant2{
Lock lock = new Lock(); public outer(){
lock.lock();
inner();
lock.unlock();
} public synchronized inner(){
lock.lock();
//do something
lock.unlock();
}
}
因为线程共享的是同一lock实例,当在inner()方法中调用lock()方法时,会发现这个Lock实例被锁住了,从而导致阻塞。
这是由于在循环语句中isLocked判断没有考虑是哪个线程锁住了它。
所以,在实现一个可重入的锁时,需要记录是哪个线程上的锁,同时还要记录上锁的次数:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0; public synchronized void lock()
throws InterruptedException{
Thread callingThread =
Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
} public synchronized void unlock(){
if(Thread.curentThread() ==
this.lockedBy){
lockedCount--; if(lockedCount == 0){
isLocked = false;
notify();
}
}
} ...
}
之所以要记录上锁的次数,是因为在unlock()调用没有达到对应lock()调用的次数之前,我们不希望锁被一次性解除。
锁的公平性
先来介绍一下线程中的不公平现象(也称作线程饥饿)
Java中导致饥饿的原因:
高优先级线程吞噬所有的低优先级线程的CPU时间。
你能为每个线程设置独自的线程优先级,优先级越高的线程获得的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你最好是不要改变其优先级值。
线程被永久堵塞在一个等待进入同步块的状态。
Java的同步代码区也是一个导致饥饿的因素。Java的同步代码区对哪个线程允许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,因为其他线程总是能持续地先于它获得访问,这即是“饥饿”问题,而一个线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)。
如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。
为避免发生上述线程饥饿导致的不公平现象,提高等待线程的公平性,我们使用锁方式来替代同步块。
以下引自 饥饿和公平:
在Lock类的实现中,有一个公平锁Fairlock。它的基本实现思路为:让每个线程在不同的对象上调用wait(),使得只有一个线程在每个对象上调用wait(),Lock类可以决定哪个对象能对其调用notify(),从而实现有效的选择唤醒哪个线程。
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List<QueueObject> waitingThreads =
new ArrayList<QueueObject>(); public void lock() throws InterruptedException{
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized(this){
waitingThreads.add(queueObject);
} while(isLockedForThisThread){
synchronized(this){
isLockedForThisThread =
isLocked || waitingThreads.get(0) != queueObject;
if(!isLockedForThisThread){
isLocked = true;
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try{
queueObject.doWait();
}catch(InterruptedException e){
synchronized(this) { waitingThreads.remove(queueObject); }
throw e;
}
}
} public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if(waitingThreads.size() > 0){
waitingThreads.get(0).doNotify();
}
}
}FairLock
public class QueueObject { private boolean isNotified = false; public synchronized void doWait() throws InterruptedException { while(!isNotified){
this.wait();
} this.isNotified = false; } public synchronized void doNotify() {
this.isNotified = true;
this.notify();
} public boolean equals(Object o) {
return this == o;
} }QueueObject
首先注意到lock()方法不在声明为synchronized,取而代之的是对必需同步的代码,在synchronized中进行嵌套。
FairLock新创建了一个QueueObject的实例,并对每个调用lock()的线程进行入队列。调用unlock()的线程将从队列头部获取QueueObject,并对其调用doNotify(),以唤醒在该对象上等待的线程。通过这种方式,在同一时间仅有一个等待线程获得唤醒,而不是所有的等待线程。这也是实现FairLock公平性的核心所在。
请注意,在同一个同步块中,锁状态依然被检查和设置,以避免出现滑漏条件。
还需注意到,QueueObject实际是一个semaphore。doWait()和doNotify()方法在QueueObject中保存着信号。这样做以避免一个线程在调用queueObject.doWait()之前被另一个调用unlock()并随之调用queueObject.doNotify()的线程重入,从而导致信号丢失。queueObject.doWait()调用放置在synchronized(this)块之外,以避免被monitor嵌套锁死,所以另外的线程可以解锁,只要当没有线程在lock方法的synchronized(this)块中执行即可。
最后,注意到queueObject.doWait()在try – catch块中是怎样调用的。在InterruptedException抛出的情况下,线程得以离开lock(),并需让它从队列中移除。
在finally语句中调用unlock()
如果用Lock来保护临界区,并且临界区有可能会抛出异常,那么在finally语句中调用unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:
lock.lock();
try{
//do critical section code,
//which may throw exception
} finally {
lock.unlock();
}
这个简单的结构可以保证当临界区抛出异常时Lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会导致其它所有在该Lock对象上调用lock()的线程一直阻塞。
Lock与synchronized的比较
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
Java.util.Lock
关于Lock的说明
Lock
框架是同步的兼容替代品,它提供了 synchronized
没有提供的许多特性,它的实现在争用下提供了更好的性能。但是,这些明显存在的好处,还不足以成为用 ReentrantLock
代替 synchronized
的理由。相反,应当根据您是否 需要 ReentrantLock
的能力来作出选择。大多数情况下,您不应当选择它 —— synchronized 工作得很好,可以在所有 JVM 上工作,更多的开发人员了解它,而且不太容易出错。只有在真正需要 Lock
的时候才用它。
相关资料:
【Java并发系列】----JUC之Lock的更多相关文章
- Java多线程系列--“JUC锁”03之 公平锁(一)
概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...
- Java多线程系列--“JUC集合”04之 ConcurrentHashMap
概要 本章是JUC系列的ConcurrentHashMap篇.内容包括:ConcurrentHashMap介绍ConcurrentHashMap原理和数据结构ConcurrentHashMap函数列表 ...
- Java多线程系列--“JUC集合”08之 LinkedBlockingQueue
概要 本章介绍JUC包中的LinkedBlockingQueue.内容包括:LinkedBlockingQueue介绍LinkedBlockingQueue原理和数据结构LinkedBlockingQ ...
- Java多线程系列--“JUC集合”09之 LinkedBlockingDeque
概要 本章介绍JUC包中的LinkedBlockingDeque.内容包括:LinkedBlockingDeque介绍LinkedBlockingDeque原理和数据结构LinkedBlockingD ...
- Java 并发系列之二:java 并发机制的底层实现原理
1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...
- Java多线程系列--“JUC锁”04之 公平锁(二)
概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...
- Java多线程系列--“JUC锁”10之 CyclicBarrier原理和示例
概要 本章介绍JUC包中的CyclicBarrier锁.内容包括:CyclicBarrier简介CyclicBarrier数据结构CyclicBarrier源码分析(基于JDK1.7.0_40)Cyc ...
- Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock
本章对ReentrantLock包进行基本介绍,这一章主要对ReentrantLock进行概括性的介绍,内容包括:ReentrantLock介绍ReentrantLock函数列表ReentrantLo ...
- Java多线程系列--“JUC锁”01之 框架
本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...
- Java多线程系列--“JUC锁”06之 Condition条件
概要 前面对JUC包中的锁的原理进行了介绍,本章会JUC中对与锁经常配合使用的Condition进行介绍,内容包括:Condition介绍Condition函数列表Condition示例转载请注明出处 ...
随机推荐
- R语言之脸谱图
脸谱图和星图类似,但它却比星图可以表示更多的数据维度.用脸谱来分析多维度数据,即将P个维度的数据用人脸部位的形状或大小来表征.脸谱图在平面上能够形象的表示多维度数据并给人以直观的印象,可帮助使用者形象 ...
- 前端技术之:JSON.stringfy详细说明
JSON.stringify() 语法JSON.stringify(value[, replacer[, space]]) value 被序列化为字符串的对象 replacer 根据类型不同,其行为也 ...
- [BZOJ4310] 跳蚤 SAM || SA
没有代码的. 传送门 先二分出第 \(mid\) 大的字串 \(s\),然后从后往前切割,每次大于 \(s\) 了就不行. 涉及到的操作:求第 \(mid\) 大子串:比较两个字串(求 \(lcp\) ...
- 关于js中函数的一点总结
1函数中this作用域 this根据当前环境来决定作用域,可以使用call和apply的方法来改变当前的this指向 <script> var name = "global&qu ...
- vue+element UI递归方式实现多级导航菜单
介绍 这是一个是基于element-UI的导航菜单组件基础上,进行了二次封装的菜单组件,该组件以组件递归的方式,实现了可根据从后端接收到的json菜单数据,动态渲染多级菜单的功能. 使用方法 由于该组 ...
- jdbc 加载数据库驱动如何破坏双亲委托模式
导读 通过jdbc链接数据库,是每个学习Java web 方向的人必然一开始会写的代码,虽然现在各路框架都帮大家封装好了jdbc,但是研究一下jdbc链接的套路还是很意义 术语以及相 ...
- day1-python初识以及变量
一.变量:将输入的内容赋值给变量,即变量=输入的内容 n1=input('请输入用户名:') 二. 变量名可以是 -英文. -数字.数字不能开头 -下划线,但是不可以下划线开头 不能是关键字 'and ...
- SpringBoot系列之@Conditional注解用法简介
SpringBoot系列之@Conditional注解用法简介 引用Spring官方文档的说法介绍一下@Conditional注解:Spring5.0.15版本@Conditional注解官方文档 @ ...
- Vue躬行记(8)——Vue Router
虽然Vue.js未提供路由功能,但是官方推出了Vue Router(即vue-router库),以插件的形式支持.它与Vue.js深度集成,可快速的创建单页应用(Single Page Applica ...
- Jenkins 与Docker/Kubernetes的自动化CI流水(笔记)
一.CI/CD 持续集成(continuous Integration,CI):代码合并.构建.部署.测试都在一起.不断执行这个过程,并对结果反馈. 持续部署(Continuous Deploymen ...