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. MongoDB——基本使用及集群搭建

    文章目录 什么是MongoDb? 基本概念 与关系型数据库的比较 Mongo的高效性 文件存储 基本使用 启动/连接服务 基础操作命令 高可用集群搭建 概念 环境准备 实践 应用场景 总结 什么是Mo ...

  2. Spring Cloud Alibaba系列(四)使用gateway作为服务网关

    什么是网关 在微服务架构里,服务的粒度被进一步细分,各个业务服务可以被独立的设计.开发.测试.部署和管理.这时,各个独立部署单元可以用不同的开发测试团队维护,可以使用不同的编程语言和技术平台进行设计, ...

  3. winxp系统连接服务器丢包解决方法

    winxp系统连接服务器丢包解决方法 MFC编写一个打开网页的程序,发生异常没有获取到数据. 分析步骤: 1. 用getLastError()获取到的信息,(2)- 系统找不到指定的文件. 2. 用浏 ...

  4. gdb基本命令总结

    本文介绍使用gdb调试程序的常用命令.  主要内容: [简介] [举例] [其他] [简介]  ============= GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具.如果你是在 ...

  5. Linux下如何查看硬件信息?

    我们在 Linux 下进行开发时,有时也需要知道当前的硬件信息,比如:CPU几核?使用情况?内存大小及使用情况?USB设备是否被识别?等等类似此类问题.下面良许介绍一些常用的硬件查看命令. lshw ...

  6. java命令行输入参数

    Java命令行输入参数 代码用例:命令行输入参数,并进行加法运算. public class Demo01 { public static void main(String[] args) { for ...

  7. JDK8--08:Optional

    在程序运行时,空指针异常应该是最常见的异常之一,因此JDK8提供了Optional来避免空指针异常. 首先说明JDK8新增的Optional及相关方法的使用 Optional的常用操作: Option ...

  8. CListCtrl 控件即使跟新数据,即时刷新以及属性设置

    用 m_CtrItem.Update( i );来即使跟新每行的数据,因为有时用某些函数如SetItemText()来设置某一行一列的数据是,控件上面的显示数据没有即使跟新,这是就有update来跟新 ...

  9. 一个比CBitmap更优秀的类 -- CImage类

    Visual C++的CBitmap类的功能是比较弱的,它只能显示出在资源中的图标.位图.光标以及图元文件的内容,而不像VB中的Image控件可以显示出绝大多数的外部图像文件(BMP.GIF.JPEG ...

  10. python之浅谈编程语言

    一.编程语言的分类 机器语言(低级语言) 可以直接和硬件交互,用0和1和计算机沟通 优点:执行效率高(因为可以直接和计算机沟通) 缺点:开发效率低(都是用0和1进行编码) 汇编语言 同样可以直接和硬件 ...