J.U.C体系进阶(二):juc-locks 锁框架
Java - J.U.C体系进阶
作者:Kerwin
邮箱:806857264@qq.com
说到做到,就是我的忍道!
juc-locks 锁框架
接口说明
Lock接口
类型 | 名称 |
---|---|
void | lock() |
void | lockInterruptibly () |
Condition | newCondition() |
boolean | tryClock() |
boolean | tryClock(Long time, TimeUnit unit) |
void | unlock() |
lock()方法类似于使用synchronized关键字加锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
lockInterruptibly()方法顾名思义,就是如果锁不可用,那么当前正在等待的线程是可以被中断的,这比synchronized关键字更加灵活。
Condition接口
可以看做是Obejct类的wait()、notify()、notifyAll()方法的替代品,与Lock配合使用
核心方法 -> awit() signal() signalAll()
ReadWriteLock接口
核心方法 -> readLock() writeLock() 获取读锁和写锁,注意除非使用Java8新锁,否则读读不互斥,读写是互斥的
ReentrantLock类使用
ReentrantLock的使用非常简单,Demo如下:
/***
* TestDemo
* @author 柯贤铭
* @date 2019年4月22日
* @email 806857264@qq.com
*/
public class ReadWriteLockTest {
private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final WriteLock writeLock = readWriteLock.writeLock();
private static final ReadLock readLock = readWriteLock.readLock();
private static final ExecutorService pool = Executors.newFixedThreadPool(50);
private static int surplusTickets = 100;// 余票量
private static int surplusThread = 500;// 统计进程执行量,在进程都执行完毕后才关闭主线程
/**
* 运行多线程,进行模拟抢票,并计算执行时间
*/
public static void main(String[] args) {
Date beginTime = new Date();
for (int i = 0; i < surplusThread; i++) {
final int runNum = i;
pool.execute(new Runnable() {
public void run() {
boolean getted = takeTicket();
String gettedMsg = "";
if (getted) {
gettedMsg = "has getted";
} else {
gettedMsg = "not getted";
}
System.out.println("thread " + runNum + " " + gettedMsg + ", remain: " + surplusTickets
+ ", line up:" + surplusThread + "..");
}
});
}
while (surplusThread >= 30) {
sleep(100);
}
Date overTime = new Date();
System.out.println("take times:" + (overTime.getTime() - beginTime.getTime()) + " millis.");
}
/**
* 查询当前的余票量
*/
private static int nowSurplus() {
readLock.lock();
int s = surplusTickets;
sleep(30);// 模拟复杂业务
readLock.unlock();
return s;
}
/**
* 拿出一张票
*/
private static boolean takeTicket() {
writeLock.lock();
boolean result = false;
if (nowSurplus() > 0) {
surplusTickets -= 1;
result = true;
}
surplusThread -= 1;
writeLock.unlock();
return result;
}
/**
* 睡觉觉
*/
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
说明: 关键点就在获取其读锁和写锁上,为什么要区分?
因为读写互斥,读读不互斥,所以如果不分清楚的话就会让只读操作性能大大下降
另外: 在频繁互斥情况下,其实Lock的性能和synchronized是一样的
但这仅限于在PC端(用新型编译器和虚拟机),如果是在安卓端,synchronized会慢十几倍
LockSupport工具类
Doug Lea 的神作concurrent包是基于AQS (AbstractQueuedSynchronizer)框架,AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。因此,LockSupport可谓构建concurrent包的基础之一。理解concurrent包,就从这里开始。
归根结底,LockSupport调用的Unsafe中的native代码:
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute, long time);
两个函数声明清楚地说明了操作对象:
park函数是将当前Thread阻塞,而unpark函数则是将另一个Thread唤醒。
与Object类的wait/notify机制相比,park/unpark有两个优点:
- 以thread为操作对象更符合阻塞线程的直观定义;
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性
举个例子,假设现在需要实现一种FIFO类型的独占锁,可以把这种锁看成是ReentrantLock的公平锁简单版本,且是不可重入的,就是说当一个线程获得锁后,其它等待线程以FIFO的调度方式等待获取锁 :
public class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);
private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
public void lock() {
Thread current = Thread.currentThread();
waiters.add(current);
// 如果当前线程不在队首,或锁已被占用,则当前线程阻塞
// NOTE:这个判断的意图其实就是:锁必须由队首元素拿到
while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
LockSupport.park(this);
}
waiters.remove(); // 删除队首元素
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
测试代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
FIFOMutex mutex = new FIFOMutex();
MyThread a1 = new MyThread("a1", mutex);
MyThread a2 = new MyThread("a2", mutex);
MyThread a3 = new MyThread("a3", mutex);
a1.start();
a2.start();
a3.start();
a1.join();
a2.join();
a3.join();
assert MyThread.count == 300;
System.out.print("Finished");
}
}
class MyThread extends Thread {
private String name;
private FIFOMutex mutex;
public static int count;
public MyThread(String name, FIFOMutex mutex) {
this.name = name;
this.mutex = mutex;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
mutex.lock();
count++;
System.out.println("name:" + name + " count:" + count);
mutex.unlock();
}
}
}
park方法的调用一般要方法一个循环判断体里面。
如上述示例中的:while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
LockSupport.park(this);
}
之所以这样做,是为了防止线程被唤醒后,不进行判断而意外继续向下执行,这其实是一种的多线程设计模式-Guarded Suspension
AbstractQueuedSynchronizer抽象类
AbstractQueuedSynchronizer抽象类是整个JUC体系的核心,一两句话说不清,如果仅限于使用JUC的话,其实也不用看,如果想知道源码层的话,推荐以下几个博文:
核心:抽象类采用模板方法模式主要解决何时,何线程,在何状态下 -> acquire和release的问题 获取资源与释放资源
StampedLock Java8新型锁
ReentrantReadWriteLock锁具有读写锁,问题在于ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的 ,很容易造成写锁获取不到资源
解决的必要问题:读锁采用乐观锁机制,非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程,但是API稍微复杂,因此使用时需要注意
StampedLock的主要特点概括一下,有以下几点:
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。
StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
无论写锁还是读锁,都不支持Conditon等待
/***
* TestStampedLock
* @author 柯贤铭
* @date 2019年4月22日
* @email 806857264@qq.com
*/
public class TestStampedLock {
// StampedLock锁
private static final StampedLock sLock = new StampedLock();
// 模拟500张票
private static Integer total = 500;
// 模拟100个人
private static Integer person = 100;
private static final ExecutorService pool = Executors.newFixedThreadPool(person);
private static final CountDownLatch LATCH = new CountDownLatch(person);
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
for (int i = 0; i < person; i++) {
final Integer index = i;
pool.execute(new Runnable() {
@Override
public void run() {
Integer sheng = TestStampedLock.read();
if (sheng >= 1) {
try {
boolean flag = TestStampedLock.buy();
if (flag) {
System.out.println("线程 " + index + "买到了");
} else {
System.out.println("线程 " + index + "no no no");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("线程 " + index + "no no no");
}
LATCH.countDown();
}
});
}
LATCH.await();
Long end = System.currentTimeMillis();
System.out.println("一共耗时: " + (end - start));
}
/**
* 读剩余还有几张票
* @return
*/
private static Integer read () {
Long stamp = sLock.tryOptimisticRead();
Integer piao = total;
if (!sLock.validate(stamp)) {
stamp = sLock.readLock();
try {
piao = total;
} finally {
sLock.unlockRead(stamp);
}
}
return piao;
}
/***
* 买票
* @throws InterruptedException
*/
private static boolean buy () throws InterruptedException {
Long stamp = sLock.writeLock();
// 模拟复杂操作
Thread.sleep(30);
try {
if (total >= 1) {
total--;
return true;
}
} finally {
sLock.unlockWrite(stamp);
}
return false;
}
}
StampedLock乐观锁操作必要步骤:
// 注意:StampedLock的必要操作流程
// 唯一需要注意的地方就是乐观读锁的地方 - 官方Demo
double distanceFormOrigin() {//只读方法
//试图尝试一次乐观读 返回一个类似于时间戳的邮戳整数stamp
long stamp = s1.tryOptimisticRead();
//读取x和y的值,这时候我们并不确定x和y是否是一致的
double currentX = x, currentY = y;
//判断这个stamp是否在读过程发生期间被修改过,如果stamp没有被修改过,责任无这次读取时有效的,因此就可以直接return了,反之,如果stamp是不可用的,则意味着在读取的过程中,可能被其他线程改写了数据,因此,有可能出现脏读,如果如果出现这种情况,我们可以像CAS操作那样在一个死循环中一直使用乐观锁,知道成功为止
if (!s1.validate(stamp)) {
//也可以升级锁的级别,这里我们升级乐观锁的级别,将乐观锁变为悲观锁, 如果当前对象正在被修改,则读锁的申请可能导致线程挂起.
stamp = s1.readLock();
try {
currentX = x;
currentY = y;
} finally {
s1.unlockRead(stamp);//退出临界区,释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
J.U.C体系进阶(二):juc-locks 锁框架的更多相关文章
- J.U.C体系进阶(四):juc-sync 同步器框架
Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-sync 同步器框架 同步器名称 作用 CountDownLatch 倒 ...
- J.U.C体系进阶(五):juc-collections 集合框架
Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-collections 集合框架 ConcurrentHashMap C ...
- J.U.C体系进阶(三)- juc-atomic 原子类框架
Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-atomic 原子类框架 AtomicInteger AtomicInt ...
- J.U.C体系进阶(一):juc-executors 执行器框架
Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! 主要内容: juc-executors 执行器框架 juc-locks 锁框架 ...
- 【JUC】JUC锁框架综述
一.前言 在分析完了集合框架后,很有必要接着分析java并发包下面的源码,JUC(java.util.concurrent)源码也是我们学习Java迈进一步的重要过程.我们分为几个模块进行分析,首先是 ...
- mysql进阶(二十九)常用函数
mysql进阶(二十九)常用函数 一.数学函数 ABS(x) 返回x的绝对值 BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制) CEILING(x) 返回大于x的最小整数值 EXP ...
- mysql进阶(二十八)MySQL GRANT REVOKE用法
mysql进阶(二十八)MySQL GRANT REVOKE用法 MySQL的权限系统围绕着两个概念: 认证->确定用户是否允许连接数据库服务器: 授权->确定用户是否拥有足够的权限执 ...
- mysql进阶(二十七)数据库索引原理
mysql进阶(二十七)数据库索引原理 前言 本文主要是阐述MySQL索引机制,主要是说明存储引擎Innodb. 第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础. ...
- mysql进阶(二十六)MySQL 索引类型(初学者必看)
mysql进阶(二十六)MySQL 索引类型(初学者必看) 索引是快速搜索的关键.MySQL 索引的建立对于 MySQL 的高效运行是很重要的.下面介绍几种常见的 MySQL 索引类型. 在数 ...
随机推荐
- Spring Boot 在启动时进行配置文件加解密
Spring Boot Application 事件和监听器 寻找到application.yml的读取的操作. 从spring.factories 中查看到 # Application Listen ...
- numpy中的max和maximum
numpy科学计算包中有两个函数np.max()和np.maximum(),他们的功能截然不同.简单而言即前者作用于ndarray对象,求的是它自身的最大.而后者是一个数学上的取$\max$的效果,它 ...
- 重识Java8函数式编程
前言 最近真的是太忙忙忙忙忙了,很久没有更新文章了.最近工作中看到了几段关于函数式编程的代码,但是有点费解,于是就准备总结一下函数式编程.很多东西很简单,但是如果不总结,可能会被它的各种变体所困扰.接 ...
- Windwos安装Redis
下载地址:https://github.com/MicrosoftArchive/redis 进入后点击release,下方可看到下载地址,下载mis文件,双击即可安装
- v-model指令的学习(双向绑定)
v-bind 只能实现数据的单项绑定,从data到view,无法实现双向绑定 v-model可以实现表单元素和model中数据的双向绑定 注意:model只能运用到表单元素中 如:input sele ...
- 尚硅谷ajax视频教程1
1.+尚硅谷_佟刚_Ajax_概述.wmv 2.+尚硅谷_佟刚_Ajax_使用+XMLHttpRequest+实现+Ajax.wmv XMLHttpRequest 对象提供了对 HTTP 协议的完全的 ...
- Shell 脚本学习(1)
一 Shell概览 1. 自动化批量系统初始化程序(update, 软件安装,时区设置,安全策略,...) 2. 自动化批量软件部署程序(LAMP,LNMP,Tomcat,LVS,Nginx) 3. ...
- SQL中的ON DUPLICATE KEY UPDATE使用详解
一:主键索引,唯一索引和普通索引的关系主键索引 主键索引是唯一索引的特殊类型. 数据库表通常有一列或列组合,其值用来唯一标识表中的每一行.该列称为表的主键. 在数据库关系图中为表定义一个主键将自动创建 ...
- 洛谷 P1186 【玛丽卡】
这道题题目真的想吐槽一下...是在机房同学的解释下才看懂的.就是让你求在可以删一条边的情况下,并且删后保证可以到达终点时,求删了后的最大的最短路径. 70分暴力思路: 枚举删边,然后跑一下最短路即可, ...
- 查看笔记本SN序列号
https://www.pstips.net/getting-computer-serial-number.html $ComputerName = $env:COMPUTERNAME $serial ...