作者:汤圆

个人博客:javalover.cc

简介

悲观锁和乐观锁都属于比较抽象的概念;

我们可以用拟人的手法来想象一下:

  • 悲观锁:像有些人,凡事都往坏的想,做最坏的打算;在java中就表现为,总是认为其他线程会去修改共享数据,所以每次操作共享数据时,都要加锁(比如我们前面介绍过的内置锁显式锁
  • 乐观锁:像乐天派,凡事都往好的想,做最好的打算;在Java中就表现为,总是认为其他线程都不会去修改共享数据,所以每次操作共享数据时,都不加锁,而是通过判断当前状态和上一次的状态,来进行下一步的操作;(比如这节要介绍的无锁,其中最常见的实现就是CAS算法)

目录

  1. 乐观锁的简单实现:CAS
  2. 乐观锁的优点&缺点
  3. 乐观锁的适用场景

正文

1. 乐观锁的简单实现:CAS

CAS的实现原理是比较并交换,简单点来说就是,更新数据之前,会先检查数据是否有被修改过:

  • 如果没有修改,则直接更新;
  • 如果有被修改过,则重试;

下面我们通过一个代码来看下CAS的应用,这里举的例子是原子类AtomicInteger

  1. public class AtomicDemo {
  2. public static void main(String[] args) {
  3. AtomicInteger atomicInteger = new AtomicInteger(1);
  4. ExecutorService service = Executors.newFixedThreadPool(10);
  5. for (int i = 0; i < 100; i++) {
  6. service.submit(()->{
  7. // 这里会先检查AtomicInteger中的值是否被修改,如果没被修改,才会更新,否则会自旋等待
  8. atomicInteger.getAndIncrement();
  9. });
  10. }
  11. try {
  12. TimeUnit.SECONDS.sleep(1);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println(atomicInteger.get());
  17. }
  18. }

可以看到,输出的永远都是101,说明结果符合预期;

这里我们看下getAndIncrement的源码,如下所示:

  1. // AtomicInteger.java
  2. public final int getAndIncrement() {
  3. return unsafe.getAndAddInt(this, valueOffset, 1);
  4. }
  5. // UnSafe.java
  6. public final int getAndAddInt(Object var1, long var2, int var4) {
  7. int var5;
  8. // 这里就是上面的CAS算法核心
  9. do {
  10. // 1. 先取出期望值 var5(var1为值所在的对象,var2为字段在对象中的位移量)
  11. var5 = this.getIntVolatile(var1, var2);
  12. // 2. 然后赋值时,获取当前值,跟刚才取出的期望值 var5作比较
  13. // 2.1 如果比较后发现值被修改了,则循环do while,直到当前值符合预期,才会进行更新操作(默认10次,超过10次还不符合预期,就会挂起线程,不再浪费CPU资源)
  14. // 2.2 如果比较后发现值没被修改,则直接更新
  15. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  16. // 3. 返回旧值,即期望值
  17. return var5;
  18. }

这里假设我们不是用的原子变量,而是普通的int来执行自增,那么就有可能出现结果<预期的情况(因为自增不是原子操作),比如下面的代码

  1. // 不要用这种方式来修改int值,不安全
  2. public class AtomicDemo {
  3. static int m = 1;
  4. public static void main(String[] args) {
  5. ExecutorService service = Executors.newFixedThreadPool(10);
  6. for (int i = 0; i < 1000; i++) {
  7. final int j = i;
  8. service.submit(()->{
  9. m++;
  10. });
  11. }
  12. try {
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(m);
  18. }
  19. }

多运行几次,你会发现结果可能会小于预期,所以这就是原子类的好处:不用加锁就可以实现自增等原子操作

2. 乐观锁的优点&缺点

它的优点很多,比如:

  1. 没有锁竞争,也就不会产生死锁问题
  2. 不需要来回切换线程,降低了开销(悲观锁需挂起和恢复线程,如果任务执行时间又很短,那么这个操作就会很频繁)

优点看起来还可以,那它有没有缺点呢?也是有的:

  • ABA问题:比如线程1将共享数据A改为B,然后过一会又改为A,那么此时线程2访问数据时,会认为该数据没被修改过(当前值符合预期值),这样我们就无法得知数据中间是否真的被修改过,以及修改的次数
  • 开销问题:如果自旋一直不符合预期值,那么就会一直自旋,从而导致开销很大(JDK6之前)
  • 原子操作的局限性问题:虽然CAS可以保证原子操作,但是只是针对单个数据而言的;如果有多个数据需要同

    步,CAS还是无能为力

下面我们就针对这几个缺点来提出对于的解决方案

ABA问题

出现ABA问题,主要是因为我们没有对修改过程进行记录(就好比程序中的日志记录功能)

那么我们可以通过版本号的方式来记录每次修改,比如每修改一次,给对象的版本号属性加1

不过现在有了AtomicStampedReference这个类,它帮我们封装了所需的状态值,拿来即用,如下所示:

  1. public class AtomicStampedReference<V> {
  2. private static class Pair<T> {
  3. final T reference;
  4. // 这里的stamp就是状态值,每次CAS都会同时比较当前值T和状态值stamp
  5. final int stamp;
  6. private Pair(T reference, int stamp) {
  7. this.reference = reference;
  8. this.stamp = stamp;
  9. }
  10. static <T> Pair<T> of(T reference, int stamp) {
  11. return new Pair<T>(reference, stamp);
  12. }
  13. }
  14. // 下面就是同时比较当前值和状态值
  15. public boolean compareAndSet(V expectedReference,
  16. V newReference,
  17. int expectedStamp,
  18. int newStamp) {
  19. Pair<V> current = pair;
  20. return
  21. expectedReference == current.reference &&
  22. expectedStamp == current.stamp &&
  23. ((newReference == current.reference &&
  24. newStamp == current.stamp) ||
  25. casPair(current, Pair.of(newReference, newStamp)));
  26. }
  27. }

开销问题

利用CAS进行自旋操作时,如果发现当前值一直都不等于期望值,就会一直循环(JDK6之前)

所以这里就引出了一个适应性自旋锁的概念:当尝试过N次后,发现还是不成功,则退出循环,挂起线程(JDK6之后,有了适应性自旋锁)

这里的N是不固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

---- 参考自《不可不说的Java“锁”事

大致意思就是,如果一个线程之前自旋成功过,获取过锁,那么后面就会让这个线程多自旋一会,比如20次(信用高)

但是如果如果一个线程之前自旋没成功过或者很少成功,那么后面就会让这个线程少自旋一会,比如5次(信用低)

这里需要纠正一个观点:自旋锁的次数设置问题,从JDK6开始,-XX:PreBlockSpin这个VM参数已经没有意义了,在JDK7中已经被移除了;JDK6版本之后,默认都是用适应性自旋锁来动态设置自旋的次数

如下图所示:

在IDEA中添加-XX:PreBlockSpin=1参数,运行会报错如下:

原子操作的局限性问题

CAS的原子操作只是针对单个共享变量而言的(就像前面介绍的同步容器一样,虽然每个方法都有锁,但是复合操作却无法保证原子性)

不过AtomicReference这个类会有所帮助,它内部有一个V属性,我们可以将多个共享变量封装到这个V属性中,然后再对V进行CAS操作

源码如下:

  1. public class AtomicReference<V> implements java.io.Serializable {
  2. private static final long serialVersionUID = -1848883965231344442L;
  3. private static final Unsafe unsafe = Unsafe.getUnsafe();
  4. private static final long valueOffset;
  5. static {
  6. try {
  7. valueOffset = unsafe.objectFieldOffset
  8. (AtomicReference.class.getDeclaredField("value"));
  9. } catch (Exception ex) { throw new Error(ex); }
  10. }
  11. // 这里的V我们可以自己定义一个类,然后将多个共享变量都封装进去
  12. private volatile V value;
  13. }

3. 乐观锁的适用场景

分析乐观锁的适用场景之前,我们可以先看下悲观锁的适用场景

悲观锁是一来就上锁,所以比较适合写多读少的场景,因为上了锁,可以保证数据的一致性

那么乐观锁对应的,就是从来都不上锁,所以比较适合读多写少的场景,因为读不会修改数据,所以CAS时成功的概率很大,也就不会有额外的开销

总结

  1. 乐观锁的简单实现:CAS,比较并交换
  2. 乐观锁的优点&缺点:
优点 缺点
没有锁竞争,也就不会产生死锁问题 ABA问题(加状态值解决)
不需要来回切换线程,降低了开销 自旋时间过长导致的开销问题(旧版本JDK6之前才有的问题,JDK6之后默认用适应性自旋来动态设置自旋次数)
多个共享变量不能保证原子操作(用AtomicReference封装多个共享变量)
  1. 乐观锁的适用场景:读多写少

参考

Java并发:乐观锁的更多相关文章

  1. java 并发多线程 锁的分类概念介绍 多线程下篇(二)

    接下来对锁的概念再次进行深入的介绍 之前反复的提到锁,通常的理解就是,锁---互斥---同步---阻塞 其实这是常用的独占锁(排它锁)的概念,也是一种简单粗暴的解决方案 抗战电影中,经常出现为了阻止日 ...

  2. Java并发 - (无锁)篇6

    , 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 本文主要介绍了死锁的概念与一些相关的基础类, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 无锁是一种乐观的策略, 它假设对资源的访问是没 ...

  3. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  4. 从源码学习Java并发的锁是怎么维护内部线程队列的

    从源码学习Java并发的锁是怎么维护内部线程队列的 在上一篇文章中,凯哥对同步组件基础框架- AbstractQueuedSynchronizer(AQS)做了大概的介绍.我们知道AQS能够通过内置的 ...

  5. Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...

  6. Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  7. web开发中的两把锁之数据库锁:(高并发--乐观锁、悲观锁)

    这篇文章讲了 1.同步异步概念(消去很多疑惑),同步就是一件事一件事的做:sychronized就是保证线程一个一个的执行. 2.我们需要明白,锁机制有两个层面,一种是代码层次上的,如Java中的同步 ...

  8. java 并发(六) --- 锁

          阅读前阅读以下参考资料,文章图片或代码部分来自与参考资料 概览 一张图了解一下java锁. 注 : 阻塞将会切换线程,切换内核态和用户态,是比较大的性能开销 各种锁 为什么要设置锁的等级 ...

  9. 【Java并发】锁机制

    一.重入锁 二.读写锁 三.悲观锁.乐观锁 3.1 悲观锁 3.2 乐观锁 3.3 CAS操作方式 3.4 CAS算法理解 3.5 CAS(乐观锁算法) 3.6 CAS缺点 四.原子类 4.1 概述 ...

  10. java 并发线程锁

     1.同步和异步的区别和联系 异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回 值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流 ...

随机推荐

  1. 如何在jQuery的Ajax调用后管理一个重定向请求

    1 success:function(data){ 2 if(data.xx == "xx") 3 { 4 //code... 5 window.location.href =&q ...

  2. SqlServer数据库主从同步

    分发/订阅模式实现SqlServer主从同步 在文章开始之前,我们先了解一下几个关键的概念: 分发服务器分发服务器是负责存储在同步过程中所用复制信息的服务器.可以比喻成报刊发行商. 分发数据库分发数据 ...

  3. PowerDesigner16安装和使用

    安装 安装参考链接:PowerDesigner安装教程 因为这个博主已经操作的很详细了,这边就不做过多的赘述. 使用 新建模型 选择物理模型 调出面板Palette 建表 最终的效果(一般不在数据库层 ...

  4. dispatcherServlet-servlet.xml(SSM maven 项目)

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  5. 0902-用GAN生成动漫头像

    0902-用GAN生成动漫头像 目录 一.概述 二.代码结构 三.model.py 3.1 生成器 3.2 判别器 四.参数配置 五.数据处理 六.训练 七.随机生成图片 八.训练模型并测试 pyto ...

  6. NtQuerySystemInformation获取进程/线程状态

    __kernel_entry NTSTATUS NtQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, P ...

  7. 12.26vj训练补题

    D.City Day 题意:就是给定n,x,y,以及这n天的下雨量ai,要求这一天的下雨量是这一天前x天到后y天的下雨量中最小的.输出最早的(下标最小的)d.保证答案一定存在 思路:直接遍历寻找就好了 ...

  8. kvm虚拟化安装与部署(2)

    一.虚拟化VT开启确认 KVM 本身也有一些弱点,那就是相比裸金属虚拟化架构的 Xen . VMware ESX 和 HyperV , KVM 是运行在 Linux 内核之上的寄居式虚拟化架构,会消耗 ...

  9. 校准仪的开发 ---等下整理 迪文屏的ICO文件 和输出配置问题

    要有ICO文件才能

  10. nginx 的基础知识(二)

    Nginx 多进程网络模型 进程模型 nginx启动后以daemon的方式在后台运行,后台进程包括一个master进程和多个worker进程 master进程主要作用,接收来自外界的信号:向各work ...