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 锁框架的更多相关文章

  1. J.U.C体系进阶(四):juc-sync 同步器框架

    Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-sync 同步器框架 同步器名称 作用 CountDownLatch 倒 ...

  2. J.U.C体系进阶(五):juc-collections 集合框架

    Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-collections 集合框架 ConcurrentHashMap C ...

  3. J.U.C体系进阶(三)- juc-atomic 原子类框架

    Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! juc-atomic 原子类框架 AtomicInteger AtomicInt ...

  4. J.U.C体系进阶(一):juc-executors 执行器框架

    Java - J.U.C体系进阶 作者:Kerwin 邮箱:806857264@qq.com 说到做到,就是我的忍道! 主要内容: juc-executors 执行器框架 juc-locks 锁框架 ...

  5. 【JUC】JUC锁框架综述

    一.前言 在分析完了集合框架后,很有必要接着分析java并发包下面的源码,JUC(java.util.concurrent)源码也是我们学习Java迈进一步的重要过程.我们分为几个模块进行分析,首先是 ...

  6. mysql进阶(二十九)常用函数

    mysql进阶(二十九)常用函数 一.数学函数 ABS(x) 返回x的绝对值 BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制) CEILING(x) 返回大于x的最小整数值 EXP ...

  7. mysql进阶(二十八)MySQL GRANT REVOKE用法

    mysql进阶(二十八)MySQL GRANT REVOKE用法   MySQL的权限系统围绕着两个概念: 认证->确定用户是否允许连接数据库服务器: 授权->确定用户是否拥有足够的权限执 ...

  8. mysql进阶(二十七)数据库索引原理

    mysql进阶(二十七)数据库索引原理 前言   本文主要是阐述MySQL索引机制,主要是说明存储引擎Innodb.   第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础.    ...

  9. mysql进阶(二十六)MySQL 索引类型(初学者必看)

    mysql进阶(二十六)MySQL 索引类型(初学者必看)   索引是快速搜索的关键.MySQL 索引的建立对于 MySQL 的高效运行是很重要的.下面介绍几种常见的 MySQL 索引类型.   在数 ...

随机推荐

  1. Spring Boot 在启动时进行配置文件加解密

    Spring Boot Application 事件和监听器 寻找到application.yml的读取的操作. 从spring.factories 中查看到 # Application Listen ...

  2. numpy中的max和maximum

    numpy科学计算包中有两个函数np.max()和np.maximum(),他们的功能截然不同.简单而言即前者作用于ndarray对象,求的是它自身的最大.而后者是一个数学上的取$\max$的效果,它 ...

  3. 重识Java8函数式编程

    前言 最近真的是太忙忙忙忙忙了,很久没有更新文章了.最近工作中看到了几段关于函数式编程的代码,但是有点费解,于是就准备总结一下函数式编程.很多东西很简单,但是如果不总结,可能会被它的各种变体所困扰.接 ...

  4. Windwos安装Redis

    下载地址:https://github.com/MicrosoftArchive/redis 进入后点击release,下方可看到下载地址,下载mis文件,双击即可安装

  5. v-model指令的学习(双向绑定)

    v-bind 只能实现数据的单项绑定,从data到view,无法实现双向绑定 v-model可以实现表单元素和model中数据的双向绑定 注意:model只能运用到表单元素中 如:input sele ...

  6. 尚硅谷ajax视频教程1

    1.+尚硅谷_佟刚_Ajax_概述.wmv 2.+尚硅谷_佟刚_Ajax_使用+XMLHttpRequest+实现+Ajax.wmv XMLHttpRequest 对象提供了对 HTTP 协议的完全的 ...

  7. Shell 脚本学习(1)

    一 Shell概览 1. 自动化批量系统初始化程序(update, 软件安装,时区设置,安全策略,...) 2. 自动化批量软件部署程序(LAMP,LNMP,Tomcat,LVS,Nginx) 3. ...

  8. SQL中的ON DUPLICATE KEY UPDATE使用详解

    一:主键索引,唯一索引和普通索引的关系主键索引 主键索引是唯一索引的特殊类型. 数据库表通常有一列或列组合,其值用来唯一标识表中的每一行.该列称为表的主键. 在数据库关系图中为表定义一个主键将自动创建 ...

  9. 洛谷 P1186 【玛丽卡】

    这道题题目真的想吐槽一下...是在机房同学的解释下才看懂的.就是让你求在可以删一条边的情况下,并且删后保证可以到达终点时,求删了后的最大的最短路径. 70分暴力思路: 枚举删边,然后跑一下最短路即可, ...

  10. 查看笔记本SN序列号

    https://www.pstips.net/getting-computer-serial-number.html $ComputerName = $env:COMPUTERNAME $serial ...