Java并发:乐观锁
作者:汤圆
个人博客:javalover.cc
简介
悲观锁和乐观锁都属于比较抽象的概念;
我们可以用拟人的手法来想象一下:
- 悲观锁:像有些人,凡事都往坏的想,做最坏的打算;在java中就表现为,总是认为其他线程会去修改共享数据,所以每次操作共享数据时,都要加锁(比如我们前面介绍过的内置锁和显式锁)
- 乐观锁:像乐天派,凡事都往好的想,做最好的打算;在Java中就表现为,总是认为其他线程都不会去修改共享数据,所以每次操作共享数据时,都不加锁,而是通过判断当前状态和上一次的状态,来进行下一步的操作;(比如这节要介绍的无锁,其中最常见的实现就是CAS算法)
目录
- 乐观锁的简单实现:CAS
- 乐观锁的优点&缺点
- 乐观锁的适用场景
正文
1. 乐观锁的简单实现:CAS
CAS的实现原理是比较并交换,简单点来说就是,更新数据之前,会先检查数据是否有被修改过:
- 如果没有修改,则直接更新;
- 如果有被修改过,则重试;
下面我们通过一个代码来看下CAS的应用,这里举的例子是原子类AtomicInteger
public class AtomicDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
service.submit(()->{
// 这里会先检查AtomicInteger中的值是否被修改,如果没被修改,才会更新,否则会自旋等待
atomicInteger.getAndIncrement();
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicInteger.get());
}
}
可以看到,输出的永远都是101,说明结果符合预期;
这里我们看下getAndIncrement的源码,如下所示:
// AtomicInteger.java
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// UnSafe.java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// 这里就是上面的CAS算法核心
do {
// 1. 先取出期望值 var5(var1为值所在的对象,var2为字段在对象中的位移量)
var5 = this.getIntVolatile(var1, var2);
// 2. 然后赋值时,获取当前值,跟刚才取出的期望值 var5作比较
// 2.1 如果比较后发现值被修改了,则循环do while,直到当前值符合预期,才会进行更新操作(默认10次,超过10次还不符合预期,就会挂起线程,不再浪费CPU资源)
// 2.2 如果比较后发现值没被修改,则直接更新
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// 3. 返回旧值,即期望值
return var5;
}
这里假设我们不是用的原子变量,而是普通的int来执行自增,那么就有可能出现结果<预期的情况(因为自增不是原子操作),比如下面的代码
// 不要用这种方式来修改int值,不安全
public class AtomicDemo {
static int m = 1;
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int j = i;
service.submit(()->{
m++;
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m);
}
}
多运行几次,你会发现结果可能会小于预期,所以这就是原子类的好处:不用加锁就可以实现自增等原子操作
2. 乐观锁的优点&缺点
它的优点很多,比如:
- 没有锁竞争,也就不会产生死锁问题
- 不需要来回切换线程,降低了开销(悲观锁需挂起和恢复线程,如果任务执行时间又很短,那么这个操作就会很频繁)
优点看起来还可以,那它有没有缺点呢?也是有的:
- ABA问题:比如线程1将共享数据A改为B,然后过一会又改为A,那么此时线程2访问数据时,会认为该数据没被修改过(当前值符合预期值),这样我们就无法得知数据中间是否真的被修改过,以及修改的次数
- 开销问题:如果自旋一直不符合预期值,那么就会一直自旋,从而导致开销很大(JDK6之前)
- 原子操作的局限性问题:虽然CAS可以保证原子操作,但是只是针对单个数据而言的;如果有多个数据需要同
步,CAS还是无能为力
下面我们就针对这几个缺点来提出对于的解决方案
ABA问题
出现ABA问题,主要是因为我们没有对修改过程进行记录(就好比程序中的日志记录功能)
那么我们可以通过版本号的方式来记录每次修改,比如每修改一次,给对象的版本号属性加1
不过现在有了AtomicStampedReference
这个类,它帮我们封装了所需的状态值,拿来即用,如下所示:
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
// 这里的stamp就是状态值,每次CAS都会同时比较当前值T和状态值stamp
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
// 下面就是同时比较当前值和状态值
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
开销问题
利用CAS进行自旋操作时,如果发现当前值一直都不等于期望值,就会一直循环(JDK6之前)
所以这里就引出了一个适应性自旋锁的概念:当尝试过N次后,发现还是不成功,则退出循环,挂起线程(JDK6之后,有了适应性自旋锁)
这里的N是不固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
---- 参考自《不可不说的Java“锁”事》
大致意思就是,如果一个线程之前自旋成功过,获取过锁,那么后面就会让这个线程多自旋一会,比如20次(信用高)
但是如果如果一个线程之前自旋没成功过或者很少成功,那么后面就会让这个线程少自旋一会,比如5次(信用低)
这里需要纠正一个观点:自旋锁的次数设置问题,从JDK6开始,-XX:PreBlockSpin这个VM参数已经没有意义了,在JDK7中已经被移除了;JDK6版本之后,默认都是用适应性自旋锁来动态设置自旋的次数
如下图所示:
在IDEA中添加-XX:PreBlockSpin=1
参数,运行会报错如下:
原子操作的局限性问题
CAS的原子操作只是针对单个共享变量而言的(就像前面介绍的同步容器一样,虽然每个方法都有锁,但是复合操作却无法保证原子性)
不过AtomicReference
这个类会有所帮助,它内部有一个V属性,我们可以将多个共享变量封装到这个V属性中,然后再对V进行CAS操作
源码如下:
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 这里的V我们可以自己定义一个类,然后将多个共享变量都封装进去
private volatile V value;
}
3. 乐观锁的适用场景
分析乐观锁的适用场景之前,我们可以先看下悲观锁的适用场景
悲观锁是一来就上锁,所以比较适合写多读少的场景,因为上了锁,可以保证数据的一致性
那么乐观锁对应的,就是从来都不上锁,所以比较适合读多写少的场景,因为读不会修改数据,所以CAS时成功的概率很大,也就不会有额外的开销
总结
- 乐观锁的简单实现:CAS,比较并交换
- 乐观锁的优点&缺点:
优点 | 缺点 |
---|---|
没有锁竞争,也就不会产生死锁问题 | ABA问题(加状态值解决) |
不需要来回切换线程,降低了开销 | 自旋时间过长导致的开销问题(旧版本JDK6之前才有的问题,JDK6之后默认用适应性自旋来动态设置自旋次数) |
多个共享变量不能保证原子操作(用AtomicReference封装多个共享变量) |
- 乐观锁的适用场景:读多写少
参考
- 《实战Java高并发》
- 不得不说的Java琐事
- 自旋次数的设置问题:-XX:PreBlockSpin
Java并发:乐观锁的更多相关文章
- java 并发多线程 锁的分类概念介绍 多线程下篇(二)
接下来对锁的概念再次进行深入的介绍 之前反复的提到锁,通常的理解就是,锁---互斥---同步---阻塞 其实这是常用的独占锁(排它锁)的概念,也是一种简单粗暴的解决方案 抗战电影中,经常出现为了阻止日 ...
- Java并发 - (无锁)篇6
, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 本文主要介绍了死锁的概念与一些相关的基础类, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 无锁是一种乐观的策略, 它假设对资源的访问是没 ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- 从源码学习Java并发的锁是怎么维护内部线程队列的
从源码学习Java并发的锁是怎么维护内部线程队列的 在上一篇文章中,凯哥对同步组件基础框架- AbstractQueuedSynchronizer(AQS)做了大概的介绍.我们知道AQS能够通过内置的 ...
- Java并发编程锁之独占公平锁与非公平锁比较
Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...
- Java并发编程锁系列之ReentrantLock对象总结
Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...
- web开发中的两把锁之数据库锁:(高并发--乐观锁、悲观锁)
这篇文章讲了 1.同步异步概念(消去很多疑惑),同步就是一件事一件事的做:sychronized就是保证线程一个一个的执行. 2.我们需要明白,锁机制有两个层面,一种是代码层次上的,如Java中的同步 ...
- java 并发(六) --- 锁
阅读前阅读以下参考资料,文章图片或代码部分来自与参考资料 概览 一张图了解一下java锁. 注 : 阻塞将会切换线程,切换内核态和用户态,是比较大的性能开销 各种锁 为什么要设置锁的等级 ...
- 【Java并发】锁机制
一.重入锁 二.读写锁 三.悲观锁.乐观锁 3.1 悲观锁 3.2 乐观锁 3.3 CAS操作方式 3.4 CAS算法理解 3.5 CAS(乐观锁算法) 3.6 CAS缺点 四.原子类 4.1 概述 ...
- java 并发线程锁
1.同步和异步的区别和联系 异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回 值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流 ...
随机推荐
- 5.IA-32寄存器
寄存器(Register)是CPU内部用来存放数据的一些小型存储区域,它与RAM(Random Access Memory,随机存储器.内存)略有不同.CPU访问(Access)RAM中的数据时要经过 ...
- 安装google cloud sdk报错解决方法总结(超全)
前提:python环境最好是2.7,如果是python3可能会出很多小毛病:能用64位系统就尽量用64位,32位系统安装也很多毛病 一.Error1:Unzip Failed: Error openi ...
- layui在toolbar使用上传控件在reload后失效的问题解决
问题描述 使用layui中的upload组件来上传文件,将按钮放了表格中的toolbar(头部工具栏中),碰到的问题是:第一次可以实现上传文件,但是第二次再上传文件的时候,点击按钮无效. 解决办法 ...
- layui的loading加载中
var load = layer.load(1, { content: '数据加载中', shade: [0.4, '#393D49'], // time: 10 * 1000, success: f ...
- .NET之生成数据库全流程
开篇语 本文主要是回顾下从项目创建到生成数据到数据库(代码优先)的全部过程.采用EFCore作为ORM框架. 本次示例环境:vs2019.net5.mysql 创建项目 本次事例代码是用过vs2019 ...
- C# 泛型Generic
泛型(Generic),是将不确定的类型预先定义下来的一种C#高级语法,我们在使用一个类,接口或者方法前,不知道用户将来传什么类型,或者我们写的类,接口或方法相同的代码可以服务不同的类型,就可以定义为 ...
- Java学习之jackson篇
Java学习之jackson篇 0x00 前言 本篇内容比较简单,简单记录. 0x01 Json 概述 概述:JSON(JavaScript Object Notation, JS 对象简谱) 是一种 ...
- 有哪些适合中小企业使用的PaaS平台?
对于中小企业来说,在业务上同样需要工作流.应用平台来进行支持,但是,面对诸如ERP等动辄好几十万的费用来说,完全是在增加运营成本.如何解决中小企业对于业务应用.工作流管理的需求问题呢?使用PaaS低代 ...
- SDK安全测试
设备调试 strace MI 5X 链接:https://pan.baidu.com/s/1KfsfEgjniozXGUD_69m0SQ 提取码:mulo 推strace到手机中 adb push s ...
- Tomcat修改jdk版本
tomcat修改jdk版本 修改tomcat bin目录下的catalina.sh和setclasspath.sh文件,添加以下内容 export JAVA_HOME=/home/nodemanage ...