为了更好的支持并发程序,JDK内部提供了多种锁。本文总结4种锁。

1.synchronized同步锁

使用

synchronized本质上就2种锁:

1.锁同步代码块

2.锁方法

可用object.wait() object.notify()来操作线程等待唤醒

原理:synchronized细节的描述传送门:jdk源码剖析三:锁Synchronized

性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。

2.ReentrantLock可重入锁(Lock接口)

使用:ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操作线程:

如上图,await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似

原理:核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

性能和建议:性能中等,建议需要手动操作线程时使用。

 

3.ReentrantReadWriteLock可重入读写锁(ReadWriteLock接口)

使用:ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。

原理:类图如下:

JDK1.8下,如图ReentrantReadWriteLock有5个静态方法:

  • Sync:继承于经典的AbstractQueuedSynchronizer(传说中的AQS),是一个抽象类,包含2个抽象方法readerShouldBlock();writerShouldBlock()
  • FairSync和NonfairSync:继承于Sync,分别实现了公平/非公平锁。
  • ReadLock和WriteLock:都是Lock实现类,分别实现了读、写锁。ReadLock是共享的,而WriteLock是独占的。于是Sync类覆盖了AQS中独占和共享模式的抽象方法(tryAcquire/tryAcquireShared等),用同一个等待队列来维护读/写排队线程,而用一个32位int state标示和记录读/写锁重入次数--Doug Lea把状态的高16位用作读锁,记录所有读锁重入次数之和,低16位用作写锁,记录写锁重入次数。所以无论是读锁还是写锁最多只能被持有65535次。

性能和建议:适用于读多写少的情况。性能较高。

  • 公平性
  1. 非公平锁(默认),为了防止写线程饿死,规则是:当等待队列头部结点是独占模式(即要获取写锁的线程)时,只有获取独占锁线程可以抢占,而试图获取共享锁的线程必须进入队列阻塞;当队列头部结点是共享模式(即要获取读锁的线程)时,试图获取独占和共享锁的线程都可以抢占。
  2. 公平锁,利用AQS的等待队列,线程按照FIFO的顺序获取锁,因此不存在写线程一直等待的问题。
  • 重入性:读写锁均是可重入的,读/写锁重入次数保存在了32位int state的高/低16位中。而单个读线程的重入次数,则记录在ThreadLocalHoldCounter类型的readHolds里。
  • 锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级。
  • 锁获取中断:读取锁和写入锁都支持获取锁期间被中断。
  • 条件变量:写锁提供了条件变量(Condition)的支持,这个和独占锁ReentrantLock一致,但是读锁却不允许,调用readLock().newCondition()会抛出UnsupportedOperationException异常。

应用:

4.StampedLock戳锁

使用

StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问。下面是JDK1.8源码自带的示例:

 public class StampedLockDemo {
//一个点的x,y坐标
private double x,y;
private final StampedLock sl = new StampedLock(); //【写锁(排它锁)】
void move(double deltaX,double deltaY) {// an exclusively locked method
/**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化
* 即每次+1,直到加到最大值,然后从0重新开始
**/
long stamp =sl.writeLock(); //写锁
try {
x +=deltaX;
y +=deltaY;
} finally {
sl.unlockWrite(stamp);//释放写锁
}
} //【乐观读锁】
double distanceFromOrigin() { // A read-only method
/**
* tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
* 每次读的时候得到一个当前的stamp值(类似时间戳的作用)
*/
long stamp = sl.tryOptimisticRead();
//这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断
double currentX = x, currentY = y;
/**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,
* 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
* 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
* 读锁又重新返回一个stampe值*/
if (!sl.validate(stamp)) {//如果验证失败(读之前已发生写)
stamp = sl.readLock(); //悲观读锁
try {
currentX = x;
currentY = y;
}finally{
sl.unlockRead(stamp);//释放读锁
}
}
//读锁验证成功后执行计算,即读的时候没有发生写
return Math.sqrt(currentX *currentX + currentY *currentY);
} //【悲观读锁】
void moveIfAtOrigin(double newX, double newY) { // upgrade
// 读锁(这里可用乐观锁替代)
long stamp = sl.readLock();
try {
//循环,检查当前状态是否符合
while (x == 0.0 && y == 0.0) {
/**
* 转换当前读戳为写戳,即上写锁
* 1.写锁戳,直接返回写锁戳
* 2.读锁戳且写锁可获得,则释放读锁,返回写锁戳
* 3.乐观读戳,当立即可用时返回写锁戳
* 4.其他情况返回0
*/
long ws = sl.tryConvertToWriteLock(stamp);
//如果写锁成功
if (ws != 0L) {
stamp = ws;// 替换票据为写锁
x = newX;//修改数据
y = newY;
break;
}
//转换为写锁失败
else {
//释放读锁
sl.unlockRead(stamp);
//获取写锁(必要情况下阻塞一直到获取写锁成功)
stamp = sl.writeLock();
}
}
} finally {
//释放锁(可能是读/写锁)
sl.unlock(stamp);
}
}
}

原理

StampedLockd的内部实现是基于CLH锁的,一种自旋锁,保证没有饥饿且FIFO。

CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。

如上图所示,StampedLockd源码中的WNote就是等待链表队列,每一个WNode标识一个等待线程,whead为CLH队列头,wtail为CLH队列尾,state为锁的状态。long型即64位,倒数第八位标识写锁状态,如果为1,标识写锁占用!下面围绕这个state来讲述锁操作。

首先是常量标识:

WBIT=1000 0000(即-128)

RBIT =0111 1111(即127)

SBIT =1000 0000(后7位表示当前正在读取的线程数量,清0)

1.乐观读

tryOptimisticRead():如果当前没有写锁占用,返回state(后7位清0,即清0读线程数),如果有写锁,返回0,即失败。

2.校验stamp

校验这个戳是否有效validate():比较当前stamp和发生乐观锁得到的stamp比较,不一致则失败。

3.悲观读

乐观锁失败后锁升级为readLock():尝试state+1,用于统计读线程的数量,如果失败,进入acquireRead()进行自旋,通过CAS获取锁。如果自旋失败,入CLH队列,然后再自旋,如果成功获得读锁,激活cowait队列中的读线程Unsafe.unpark(),最终依然失败,Unsafe().park()挂起当前线程。

4.排它写

writeLock():典型的cas操作,如果STATE等于s,设置写锁位为1(s+WBIT)。acquireWrite跟acquireRead逻辑类似,先自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程。

5.释放锁

unlockWrite():释放锁与加锁动作相反。将写标记位清零,如果state溢出,则退回到初始值;

性能和建议:JDK8之后才有,当高并发下且读远大于写时,由于可以乐观读,性能极高!

5.总结

4种锁,最稳定是内置synchronized锁(并不是完全被替代),当并发量大且读远大于写的情况下最快的的是StampedLock锁(乐观读。近似于无锁)。建议大家采用。

====================

参考:《JAVA高并发程序设计》

同步中的四种锁synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock的更多相关文章

  1. 同步中的四种锁synchronized、ReentrantLock、ReadWriteLock、StampedLock

    目录 1.synchronized同步锁 2.ReentrantLock重入锁 3.ReadWriteLock读写锁 4.StampedLock戳锁(目前没找到合适的名字,先这么叫吧...) 5.总结 ...

  2. JAVA基础学习之throws和throw的区别、Java中的四种权限、多线程的使用等(2)

    1.throws和throw的区别 throws使用在函数外,是编译时的异常,throw使用在函数内,是运行时的异常 使用方法 public int method(int[] arr) throws ...

  3. iOS中的几种锁的总结,三种开启多线程的方式(GCD、NSOperation、NSThread)

    学习内容 欢迎关注我的iOS学习总结--每天学一点iOS:https://github.com/practiceqian/one-day-one-iOS-summary OC中的几种锁 为什么要引入锁 ...

  4. 5000字 | 24张图带你彻底理解Java中的21种锁

    本篇主要内容如下: 本篇文章已收纳到我的Java在线文档. Github 我的SpringCloud实战项目持续更新中 帮你总结好的锁: 序号 锁名称 应用 1 乐观锁 CAS 2 悲观锁 synch ...

  5. 对称加密和分组加密中的四种模式(ECB、CBC、CFB、OFB)

    一. AES对称加密: AES加密 分组 二. 分组密码的填充 分组密码的填充 e.g.: PKCS#5填充方式 三. 流密码:   四. 分组密码加密中的四种模式: 3.1 ECB模式 优点: 1. ...

  6. Activity中的四种启动模式

    在Android中每个界面都是一个Activity,切换界面操作其实是多个不同Activity之间的实例化操作.在Android中Activity的启动模式决定了Activity的启动运行方式. An ...

  7. C++中的四种转型操作符

    在具体介绍C++中的四种转型操作符之前,我们先来说说旧式转型的缺点: ①它差点儿同意将不论什么类型转换为不论什么其它类型,这是十分拙劣的.假设每次转型都可以更精确地指明意图,则更好. ②旧式转型难以辨 ...

  8. JAVA中的四种引用以及ReferenceQueue和WeakHashMap的使用示例

    简介: 本文主要介绍JAVA中的四种引用: StrongReference(强引用).SoftReferenc(软引用).WeakReferenc(弱引用).PhantomReference(虚引用) ...

  9. C语言_了解一下C语言中的四种存储类别

    C语言是一门通用计算机编程语言,应用广泛.C语言的设计目标是提供一种能以简易的方式编译.处理低级存储器.产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言. C语言中的四种存储类别:auto ...

随机推荐

  1. ubuntu下用vagrant搭建集群环境

    1.安装virtualbox 终端输入:sudo apt-get install virtualbox(事实从来都不是一番风顺的.......) 正在读取软件包列表... 完成 正在分析软件包的依赖关 ...

  2. 【转】strmbasd.lib(dllentry.obj) : error LNK2001: 无法解析的外部符号"int g_cTemplates"

    加入了DirectShow的基类链接库后,如果此时编译就会出现以下编译错误: strmbasd.lib(wxutil.obj) : error LNK2019: 无法解析的外部符号 __imp__ti ...

  3. 解决IDEA16闪退的问题

    问题描述:在编辑等使用过程中,IDEA有时会突然闪退,一脸懵逼...... 原因: 1.内存分配不够 修改home/bin/目录下面的配置文件: idea.exe.vmoptions idea64.e ...

  4. 解析JSON 注意解析数据为一个对象的情况.--加一下说明

    应用场景: 调某接口时, 获取json数据, 需要对数据进行解析 . 第一种: 只判断接口是否调用成功 { "code":"10102000", "d ...

  5. Spring Boot 揭秘与实战(三) 日志框架篇 - 如何快速集成日志系统

    文章目录 1. 默认的日志框架 logback2. 常用的日志框架 log4j 1.1. 日志级别 1.2. 日志文件 3. 源代码 Java 有很多日志系统,例如,Java Util Logging ...

  6. Python学习笔记第十四周

    目录: 一.HTML 1.概述 2.HTML 二.CSS 一.HTML 1.概述 HTML是英文Hyper Text Mark-up Lang(超文本标记语言)的缩写,他是一种制作万维网页面的标准语言 ...

  7. Python网络爬虫第一弹《Python网络爬虫相关基础概念》

    爬虫介绍 引入 之前在授课过程中,好多同学都问过我这样的一个问题:为什么要学习爬虫,学习爬虫能够为我们以后的发展带来那些好处?其实学习爬虫的原因和为我们以后发展带来的好处都是显而易见的,无论是从实际的 ...

  8. [LeetCode&Python] Problem 599. Minimum Index Sum of Two Lists

    Suppose Andy and Doris want to choose a restaurant for dinner, and they both have a list of favorite ...

  9. 远程办公的GitLab开源的员工手册:涵盖了公司价值观,内部沟通交流指南,开发流程,如何开会,写作风格指南,如何报销,如何请假,线上办公工具推荐等方方面面

    原文 :https://docs.gitlab.com.cn/ce/ 英文 :https://about.gitlab.com/handbook/ GitLab Community Edition G ...

  10. Xlua使用教程、攻略

    Xlua 使用指南 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心创新! ...