并发编程之:Lock
大家好,我是小黑,一个在互联网苟且偷生的农民工。
在之前的文章中,为了保证在并发情况下多线程共享数据的线程安全,我们会使用synchronized关键字来修饰方法或者代码块,以及在生产者消费者模式中同样使用synchronized来保证生产者和消费者对于缓冲区的原子操作。
synchronized的缺点
那么synchronized这么厉害,到底有没有什么缺点呢?主要有以下几个方面:
- 使用synchronized加锁的代码块或者方法,在线程获取锁时,会一直试图获取直到获取成功,不能中断。
- 加锁的条件只能在一个锁对象上,不支持其他条件
- 无法知道锁对象的状态,是否被锁
- synchronized锁只支持非公平锁,无法做到公平
- 对于读操作和写操作都是使用独占锁,无法支持共享锁(在读操作时共享,写操作时独占)
- synchronized锁在升级之后不支持降级,如在业务流量高峰阶段升级为重量级锁,流量降低时还是重量级,效率较低(有些JVM实现支持降级,但是降级条件极为苛刻,对于Java线程来说可基本认为是不支持降级)
- 线程间通信无法按条件进行线程的唤醒,如生产者消费者场景中生产者完成数据生产后无法做到只唤醒消费者,其他等待的生产者也会被同时唤醒
以上是我能想到的synchronized锁的一些缺点,如果你有不同的看法,欢迎私信交流。(没有留言板的痛/(ㄒoㄒ)/~~)
那么synchronized的这些问题该如何解决呢?或者有没有替代方案?答案是有的,就是使用我们今天要讲的Lock锁。
Lock的优点
Lock锁是Java.util.concurrent.locks(JUC)包中的一个接口,并且有很多不同的实现类。这些实现类基本可以完全解决上面我们说到的所有问题。
Lock锁具备以下优点:
- 支持超时获取,中断获取
- 可以按条件加锁,灵活性更高
- 支持公平和非公平锁
- 有独占锁和共享锁的实现,如读写锁
- 可以做到等待线程的精准唤醒
接下来具体看看对应的实现。
基础铺垫
在开始之前,先和大家对于一些概念做一下回顾和普及。
可重入锁
可重入锁是指锁具备可重入的特性,可重入的意思是一个线程在获取锁之后,如果再次获取锁时,可以成功获取,不会因为锁正在被占有而死锁。
synchronized锁就是可重入锁,在一个synchronized方法中递归调用本方法,可以成功获取到锁,不会死锁。
Lock锁的实现中基本也都支持可重入。
公平锁和非公平锁
公平锁指在有线程获取锁失败阻塞时,一定会让先开始阻塞的线程先执行,就好比是排队买票,排在前面的先买;
非公平锁则不保证这种公平性,就算有其他线程在阻塞等待,新来的线程也可以直接获取锁,就好比插队。
独占锁和共享锁
独占锁是指一把锁同一时间只能被一个线程持有,举个生活中的例子,我们使用打车软件打专车,那么一辆车同一时间只能让一个用户打到,这辆专车就好比是一把独占锁,被一个用户独自占有了嘛。
共享锁则不一样,一把锁可以被多个线程持有,这个就想我们打拼车,一辆拼车同一时间可以让多个用户打到,这辆拼车就是一把共享锁。
说完这些以后我们来看一下Lock接口的一些具体实现。
ReentrantLock
ReentrantLock从名称理解,就是一把可重入锁,并且它是一把独占锁,而且具有公平和非公平实现。
我们通过代码来看一下如何通过ReentrantLock来做加解锁操作。
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
try {
// do something...
}finally {
lock.unlock();
}
}
首先创建一个ReentrantLock对象,在创建时构造方法传入的boolean值控制是公平锁还是非公平锁,如果不传参数则默认是非公平锁。
调用lock()方法来进行加锁,可以看到使用try-finally代码块,在finally中进行unlock()解锁操作,这一点一定要注意,因为lock不会自己进行解锁,必须手动进行释放,为了保证锁一定可以被释放,防止发生死锁,所以要在finally中进行。这一点和synchronized有区别,使用synchronized不用关注锁的释放时机,这也是为了灵活性必须要付出的一点代价。
ReentrantLock除了通过lock()方法加锁之外,还有以下方式加锁:
- tryLock():只有在调用时它不被另一个线程占用才能获取锁
- tryLock(long timeout, TimeUnit unit) 如果在给定的等待时间内没有被另一个线程占用,并且当前线程尚未被中断,则获取该锁
- lockInterruptibly() 获取锁定,除非当前线程是interrupted
除了获取锁的方法之外,还有一些其他的方法可以获得一些锁相关的状态信息:
- isLocked() 查询此锁是否由任何线程持有
- isHeldByCurrentThread() 查询此锁是否由当前线程持有
- getOwner() 返回当前拥有此锁的线程,如果不拥有,则返回null
ReentrantLock本身是独占锁,不支持共享,那么如何做到线程的精准唤醒,我们接着说。
Condition
Condition也是JUC包下的locks包中的一个接口,提供了类似于Object的wait(),notify(),notifyAll()这样的对象监听器方法,可以与Lock的实现类配合做到线程的等待/唤醒机制,并且能够做到精准唤醒。接下来我们看下面的例子:
public class ProdConsDemo {
public static void main(String[] args) {
KFC kfc = new KFC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店员1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店员2").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consume();
}
}, "顾客1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consume();
}
}, "顾客2").start();
}
}
class KFC {
int hamburgerNum = 0;
public synchronized void product() {
while (hamburgerNum == 10) {
try {
// 数量到达最大,生产者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产一个汉堡" + (++hamburgerNum));
// 唤醒其他线程
this.notifyAll();
}
public synchronized void consume() {
while (hamburgerNum == 0) {
try {
//数量到达最小,消费者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("卖出一个汉堡" + (hamburgerNum--));
// 唤醒其他线程
this.notifyAll();
}
}
看过小黑之前文章的朋友应该还记得这个例子,KFC里的店员生产汉堡,顾客来消费,典型的生产者消费者模式,我们可以看到在上面的代码中,是使用的锁对象this的wait()和notifyAll()方法来做线程等待和唤醒。那么这里会有一个问题,就是在notifyAll()时,无法做到只唤醒消费者或者只唤醒生产者。而在线程被唤醒之后就会面临更多的线程切换,而线程切换是很消耗CPU资源的。
那么我们使用Condition和ReentrantLock来修改一下我们的代码。
class KFC {
int hamburgerNum = 0;
ReentrantLock lock = new ReentrantLock();
Condition isEmpty = lock.newCondition();
Condition isFull = lock.newCondition();
public void product() {
lock.lock();
try {
while (hamburgerNum == 10) {
// 数量到达最大,生产者等待
isFull.await();
}
System.out.println("生产一个汉堡" + (++hamburgerNum));
// 唤醒消费者线程
isEmpty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (hamburgerNum == 0) {
//数量到达最小,消费者等待
isEmpty.await();
}
System.out.println("卖出一个汉堡" + (hamburgerNum--));
// 唤醒生产者线程
isFull.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
可以看到,我们使用ReentrantLock来进行线程安全控制,进行加解锁,然后创建两个Condition对象,分别代表生产者和消费者的标记,当生产者生产完一个之后,就会准确的唤醒消费者线程,反之同理。
ReadWriteLock
ReadWriteLock是读写锁接口,通过ReadWriteLock可以实现多个线程对于读操作共享,对于写操作独占。
在ReadWriteLock中有两个Lock变量,通过两个Lock分别控制读和写。
class Data {
private int num = 0;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "读取=>" + num);
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "读取结束");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "写入=>" + num++);
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "写入结束");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
对于读写锁,线程对于锁的竞争情况如下:
- 读-读操作共享;
- 读-写操作独占;
- 写-读操作独占;
- 写-写操作独占;
也就是说,当有一个线程持有读锁时,其他线程也可以获取读到读锁,但是不能获取写锁,必须等读锁释放;当有一个线程持有写锁时,其他线程都不能获取到锁。
StampedLock
StampedLock是JDK1.8新引入的,主要是为了优化ReadWriteLock的读写锁性能,相比于普通的ReadWriteLock主要多了乐观获取读锁的功能。
那么ReadWriteLock有什么性能问题呢?主要出现在读-写操作上,当有一个线程在读取时,写线程只能等读取完之后才能获取,读的过程中不允许写,是一个悲观读锁。
StampedLock允许在读的过程中写,但是这样会导致我们读线程获取的数据不一致,所以需要增加一点代码来判断在读的过程中是否有些操作,这是一种乐观读的锁;我们来看一下代码。
class Data {
private int num = 0;
private final StampedLock lock = new StampedLock();
public void read() {
// long stamp = lock.readLock();
// 获取乐观读,拿到一个版本戳
long stamp = lock.tryOptimisticRead();
try {
System.out.println(Thread.currentThread().getName() + "读取=>" + num);
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "读取结束");
// 读取完之后对刚开始拿到的版本戳进行验证
if (!lock.validate(stamp)) {
// 验证不通过,说明发生了写操作,这是需要重新获取悲观读锁进行处理
System.out.println("validatefalse");
stamp = lock.readLock();
// do something...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlockRead(stamp);
}
}
public void write() {
long stamp = lock.writeLock();
try {
System.out.println(Thread.currentThread().getName() + "写入=>" + num++);
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "写入结束");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlockWrite(stamp);
}
}
}
所以StampedLock就是先乐观的认为在读的过程中不会有写操作,所以是乐观锁,而悲观锁就是悲观的认为在读的过程中会有些操作,所以拒绝写入。
显然在并发高的情况下乐观锁的并发效率要更高,但是会有一小部分的写入导致数据不准确,所以需要通过validate(stamp)检测出来,重新读取。
总结
简单总结一下,首先我们讲了synchronized的7个缺点:不能超时中断;只能在一个对象上加锁;获取不到锁的状态;不支持公平锁;不支持共享锁;锁升级后不能降级;无法做到精准唤醒阻塞线程等。
然后我们通过Lock的具体实现看到,Lock都解决了这些问题,ReentrantLock支持超时中断获取锁,并且可以按条件判断进行加锁,有方法可以看到锁的状态信息,支持公平和非公平实现等,通过Condition的await()和signal()/signalAll()可以做到精准唤醒等待线程;ReadWriteLock可以支持共享锁,读锁共享,写锁独占;然后StampedLock在性能上对读写锁进行优化,主要是通过乐观读锁和vaidate(stamp)验证读取过程中有没有写入。
使用Lock锁很重要的一点就是需要自己手动释放锁,所以一定要写在finally中;
使用Conditon进行唤醒线程时要记清楚是signal()/signalAll()方法,不是notify()/notifyAll()方法,不要用错了。
Lock锁的底层实现逻辑都是依赖于AbstractQueuedSynchronizer(AQS)和CAS无锁机制来实现的,这部分内容比较复杂,我们下期单独来说一说。
好的,今天的内容就到这里,我们下期见。
关注我的公众号【小黑说Java】,更多干货内容。
并发编程之:Lock的更多相关文章
- Java并发编程之Lock
重入锁ReentrantLock 可以代替synchronized, 但synchronized更灵活. 但是, 必须必须必须要手动释放锁. try { lock.lock(); } finally ...
- Java并发编程之Lock(同步锁、死锁)
这篇文章是接着我上一篇文章来的. 上一篇文章 同步锁 为什么需要同步锁? 首先,我们来看看这张图. 这是一个程序,多个对象进行抢票. package MovieDemo; public class T ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- python并发编程之multiprocessing进程(二)
python的multiprocessing模块是用来创建多进程的,下面对multiprocessing总结一下使用记录. 系列文章 python并发编程之threading线程(一) python并 ...
- python并发编程之threading线程(一)
进程是系统进行资源分配最小单元,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.进程在执行过程中拥有独立的内存单元,而多个线程共享内存等资源. 系列文章 py ...
- 并发编程之J.U.C的第一篇
并发编程之J.U.C AQS 原理 ReentrantLock 原理 1. 非公平锁实现原理 2)可重入原理 3. 可打断原理 5) 条件变量实现原理 3. 读写锁 3.1 ReentrantRead ...
- 并发编程之:Atomic
大家好,我是小黑,一个在互联网苟且偷生的农民工. 在开始讲今天的内容之前,先问一个问题,使用int类型做加减操作是不是线程安全的呢?比如 i++ ,++i,i=i+1这样的操作在并发情况下是否会有问题 ...
- [转载]并发编程之Operation Queue和GCD
并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...
- 并发编程之wait()、notify()
前面的并发编程之volatile中我们用程序模拟了一个场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一个线程t2监听list中的size,当size等于5时,t2线程 ...
随机推荐
- [WinError 10013]以一种访问权限不允许的方式做了一个访问套接字的尝试
Django报错截图如下: 原因分析:出现这种情况在Windows中很常见,就是端口被占用 解决步骤: 1:进入windows中的命令行窗口(win+R之后输入cmd就可以进去) 2:输入 net ...
- 货币兑换问题(动态规划法)——Python实现
# 动态规划法求解货币兑换问题 # 货币系统有 n 种硬币,面值为 v1,v2,v3...vn,其中 v1=1,使用总值为money的钱与之兑换,求如何使硬币的数目最少,即 x1,x2,x3... ...
- Querydsl与SpringBoot集成
Querydsl为大多数数据库提供了一种基于Java的类型安全,类SQL的查询方式.相比JPA,Querydsl能提供更加强大的查询方式,比如关联查询.相比MyBatis,Querydsl省去了XML ...
- Axure RP 9 Enterprise/Pro/Team for Mac/Windows安装破解版激活教程
Axure RP 9.0 是一款功能强大的.操作方便.专业可靠的快速原型设计工具.一款能够在这里体验最简单的设计方式,这里有着全新的升级的软件界面,更加的时尚,更加的丰富,专为每一个用户提供了便捷的设 ...
- 构建前端第13篇之---VUE的method:{}的括号未括到方法导致 _vm.linkProps is not a function
- C语言复习(一)
类型为void*的指针代表对象的地址,而不是类型 如果需要使用另一个源文件中定义的变量,那么只需要在定义变量前加上extern关键字 ex: extern int x;//x在其他文件中定义 左值表达 ...
- Solon 1.5.22 发布
Solon 是一个轻量的Java基础开发框架.强调,克制 + 简洁 + 开放的原则:力求,更小.更快.更自由的体验.支持:RPC.REST API.MVC.Job.Micro service.WebS ...
- Java基础技术JVM面试【笔记】
Java基础技术JVM面试[笔记] JVM JVM 对 java 类的使用总体上可以分为两部分:一是把静态的 class 文件加载到 JVM 内存,二是在 JVM 内存中进行 Java 类的生命周期管 ...
- 9、改善深度神经网络之正则化、Dropout正则化
首先我们理解一下,什么叫做正则化? 目的角度:防止过拟合 简单来说,正则化是一种为了减小测试误差的行为(有时候会增加训练误差).我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好 ...
- Echarts 图表位置调整
Echarts 图表的位置调整 折线图和柱状图,通过grid属性调整. grid:{ show:false, top:'20%', right:'5%', bottom:'10%', left:'10 ...