文中加入了个人理解,如有不准确的地方欢迎提出,笔者会及时的进行改正。

乐观锁与悲观锁

乐观锁: 假设数据不会发生冲突,只有在进行数据更新的才会对数据进行检查,如果冲突则更新失败并返回错误信息

悲观锁: 悲观锁与乐观锁恰恰相反,它是假设资源每次都会被修改,所以在访问资源之前都会进行上锁,这样其他人想要访问资源的时候就会被阻塞,直到锁被释放。

CAS

CAS(Compare And Swap)是乐观锁的一种实现方式。

通过 CAS 实现的轻量级锁会在想要更新变量时判断线程内存中的变量与公共内存中的变量值是否相等,如果相同则进行修改,如果不同则重新读取变量值并重复上述操作。

CAS的优缺点

CAS 的优点很明显:CAS 减少了线程之间的上下文切换带来的消耗,避免了线程一条线程占有公共资源其他线程全部都被阻塞。
CAS的缺点从上图也可以看出:如果不断有其他线程修改公共变量,本线程就会不断的自旋,浪费 CPU 资源。

ABA问题

说到 CAS 一定要谈到 ABA 问题,先通过下图了解一下什么是 ABA 问题

线程1获取的时候变量为 A,线程2将变量更新为B,线程3将变量又设置为A,此时线程1判断变量还是原来的值并将其更新为C。

这个操作看上去没有什么问题,在变量时基本数据类型时也基本不会发生问题,但是如果变量是一个引用类型,其中的字段可能发生了改变会带来严重的后果。

那么我们如何解决 ABA 问题呢?

通常我们会添加一个版本号来判断这个公共变量是否被改变,以 sql 代码为例:

update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision}

在 juc 中也提供了相应的类: AtomicStampReference

public class AtomicTest {public final static AtomicStampedReference<String> ATOMIC_REFERENCE = new AtomicStampedReference<String>("abc" , 0);public static void main(String []args) {//创建一个线程池ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(150, 150, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());for(int i = 0 ; i < 100 ; i++) {final int num = i;//先获取原来的versionfinal int stamp = ATOMIC_REFERENCE.getStamp();threadPoolExecutor.execute(()->{try {Thread.sleep(Math.abs((int)(Math.random() * 100)));} catch (InterruptedException e) {e.printStackTrace();}//比较value和version,并设置为新的值if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2" , stamp , stamp + 1)) {System.out.println("我是线程:" + num + ",我获得了锁进行了对象修改!");}});}//下面这个线程是为了将数据改回原始值,以便之后的操作threadPoolExecutor.execute(()->{int stamp = ATOMIC_REFERENCE.getStamp();while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc" , stamp , stamp + 1));System.out.println("已经改回为原始值!");});}
}

CAS的原子性

同时 CAS 的判断与写入操作必须本身保证是原子的,否则在判断和修改变量时其他线程对公共变量进行了修改又会导致数据不安全。

JUC 的 atomic 包下大量使用了 CAS ,我们通过 AtomicInteger 的底层源码来看看 CAS 是如何实现原子性的。

new AtomicInteger().getAndIncrement();

我们点进 getAndIncrement方法

private static final Unsafe U = Unsafe.getUnsafe();public final int getAndIncrement() {return U.getAndAddInt(this, VALUE, 1);
}

我们可以看到,这里实际上是调用了 Unsafe 类的方法,Unsafe 类是 Java 的一个后门,由于 java 不能直接操作内存,java 中有许多native 方法用来直接调用 c、c++的方法库从而直接操作内存,Unsafe 中的方法基本都是 native 的。

我们不断再向里点

@HotSpotIntrinsicCandidatepublic final native boolean compareAndSetInt(Object o, long offset,int expected,int x);

最终发现了一个本地的方法,这个方法调用了调用了 C、C++ 库中的方法。

实际上 C、C++ 库中的方法最终调用的是汇编语言中的 cmpxchg 方法,也就是说 cpu 本身就提供了 CAS 的相关指令。

但是 cmpxchg 指令本身也不能保证原子性,比如两个 cpu 同时进行上述的 cas 操作有可能也会在进行修改的时候被另一个 cpu 打断。但是 C、C++ 库中的方法在 cmpxchg 前加了一个 LOCK_IF_MP 使有多个CPU的时候在加上一把 lock 锁,lock锁会在一条CPU进行CAS 操作的时候锁死总线,这样其他 CPU 就无法操作。

总之 CAS 的原子性底层实现就是通过总线锁实现的。

synchronized

java 中 synchronized 就是悲观锁的一个具体的实现。

我们从 synchronized 锁的对象底层原理锁升级来解释。

锁对象分类

  • 对于普通方法,锁是当前对象
  • 对于静态方法,锁是当前对象的类的Class对象
  • 对于同步代码块,锁是括号中的对象

重量级锁底层原理

首先我们了解一下一个对象在堆内存中存储的结构:对象头实例数据对齐填充

对象头中存储了:对象Hash值、GC年龄计数器、锁的信息、指向对象类型的索引、(数组还会有数组长度)

任何一个对象都有一个 monitor 对象与之关联,而当一个线程拿到锁时,monitor对象的 _Owner 就会指向该线程,其他线程想要拿到锁就会进入 _EntryList 并被阻塞 ,当前线程释放锁后 _EntryList 中的线程会对锁进行争抢(synchronized是一种非公平锁,也就是说不管这个线程是否是先来的,在争抢锁时机会时相同的,而公平锁则是按照先来后到的方式来获取锁)。

对于同步代码块来说,代码最终会编译成 monitorenter 和 monitorexit 指令,monitorenter 对应的就是线程尝试获取锁的过程,而 monitorexit 则是释放锁的过程。

对于方法而言,它的实现方法在 JVM 中没有详细说明,但是也可用通过 monitorenter 与 monitorexit 来实现。

锁的升级

尽管 synchronized 被我们称为是重量级锁,但是自从 jdk1.6 对 synchronized 进行优化后,synchronized 就变得没有那么“重”了。

我们先得出一个结论:能使用 synchronized 就尽量使用 synchronized

具体的原因是 synchronized 在被优化过后不会一上来就使用重量级锁,而是按照:

偏向锁 -> 轻量级锁 -> 重量级锁

的顺序不断升级。

偏向锁

在有些情况下,锁并不会被不同的线程不断竞争,而是不断被一个线程获取,这样锁的获取和释放就会带来不必要的开销,所以偏向锁就是为了解决这种场景而出现的。

我们在底层原理说到过,对象头中存储了锁的相关信息,其中有 1bit 就是用于存储当前是否是偏向锁(偏向锁表示),2bit用于存储锁标志位(偏向锁对应为 01),在偏向锁的情况下还存储了当前偏向的线程ID,当一个线程想要获取锁首先会判断一下锁对象的偏向线程是否是自己,如果是则代表当前线程已经获取了锁,如果不是则会通过偏向锁标识先判断一下当前是否是偏向锁模式,如果设置了,则尝试使用 CAS 操作将对象头中的线程ID设置为自己的。

偏向锁采用了一种直到产生竞争才会释放锁的机制,也就是说,在尝试使用 CAS 操作将对象头指向自己时其他线程也进行了同样的操作,即产生了冲突,此时偏向锁就会撤销。

撤销的过程:CAS 发生冲突后,拥有偏向锁的线程会在安全点被暂停,此时会检查该线程的状态,如果不在运行则将对象头中的线程置为无锁状态;如果在运行要么对象头重新偏向其他线程,要么恢复到无锁状态,要么标记对象不适合作为偏向锁并升级,最后唤醒暂停的线程。

轻量级锁

轻量级锁的加锁和解锁都运用到了上文提到的 CAS 操作

加锁: 线程在执行同步代码前会先在虚拟机栈的栈帧中创建存储锁记录的空间,并尝试将对象头中的 Mark Work 复制到锁记录中。然后线程会尝试 CAS 操作来将对象头中的指针替换为指向当前锁记录的指针,成功则获取锁,失败则尝试自旋获取锁。

解锁: 尝试将线程中的锁记录替换回对象头,成功则表示没有竞争发生,失败则锁或膨胀为重量级锁(自旋达到一定次数)。

总结

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,执行速度和非同步代码几乎没有差别 如果线程间存在竞争,则带来了撤销锁的消耗 只有一个线程访问同步代码
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 可能会造成竞争锁带来的自旋,消耗CPU资源 追求响应时间,同步代码执行速度快
重量级锁 线程竞争不会被自旋,不会消耗CPU 线程阻塞,相应时间慢 追求吞吐量,同步代码执行速度慢

可重入锁与不可重入锁

公平锁与非公平锁

自旋锁 互斥锁 读写锁

一篇文章带你弄懂乐观锁与悲观锁,CAS原子性,synchronized底层原理相关推荐

  1. 一篇文章带你弄懂BI和大数据!

    BI(Business Intelligence),中文翻译是商务智能,是一套完整的解决方案,用来将组织中现有的数据进行有效的整合,快速准确的提供报表并提出决策依据,帮助组织做出明智的业务经营决策. ...

  2. 一篇文章带你弄懂大数据!

    一.大数据是什么? 大数据,big data,<大数据>一书对大数据这么定义,大数据是指不能用随机分析法(抽样调查)这样捷径,而采用所有数据进行分析处理. 这句话至少传递两种信息: 1.大 ...

  3. 【JAVA SE】一篇文章带你弄懂抽象类与接口

    大家好呀,我是制药

  4. 无线充电技术究竟有何神秘之处?一篇文章带你读懂什么是无线充电技术

    无线充电技术这个概念在很早之前就已经被提出了,发展至今在电子领域已经被深入研究应用,虽然还未曾大范围普及,但在消费电子领域的发展已经取得不错的成绩.手机厂商也纷纷在自家旗舰机上加入这一革新性的先进充电 ...

  5. 【python实例6.5】一篇文章让你弄懂政府工作报告词云~总结笔记

    一篇文章让你弄懂政府工作报告词云 1.安装wordcloud库 2.新时代中国特色社会主义的词云 wordcloud下载链接: https://www.lfd.uci.edu/~gohlke/pyth ...

  6. java ee 值范围_一篇文章带你读懂: Java EE

    原标题:一篇文章带你读懂: Java EE 点击上图,查看教学大纲 何为 Java EE Java EE是一个标准中间件体系结构 不要被名称"Java PlatformEnterprise ...

  7. 一篇文章带你看懂以及实现加解密技术中的信息防篡改、一码一检、过期失效、多种实现方式

    一篇文章带你看懂以及实现加解密技术中的信息防篡改.一码一检.过期失效.多实现方式 导语 一.简介 二.代码功能介绍以及源码 2.1.AbstractRsa 类 2.2 RsaUtils 类 2.3 R ...

  8. 一篇文章带你搞懂网络层(网际层)-- 地址篇

    网络层(Network Layer)是OSI模型中的第三层(TCP/IP模型中的网际层),提供路由和寻址的功能,使两终端系统能够互连且决定最佳路径,并具有一定的拥塞控制和流量控制的能力.相当于发送邮件 ...

  9. 一篇文章让你弄懂到底什么是classpath

    转自:一篇文章让你弄懂到底什么是classpath - yuan_qh - 博客园 classpath其实就是一个路径而已,我们经常在spring的配置文件中这样写: <property nam ...

最新文章

  1. HTML实现折现图完整源码及效果图
  2. linux中double大小,linux 下 float 和 double 精度计算差别
  3. 开发日记-20190519 关键词 Linux学习路径
  4. [Manifest]关于sharedUserIdsharedUserLabel
  5. PHP的SOAP原理及实现
  6. Flagger on ASM——基于Mixerless Telemetry实现渐进式灰度发布系列 1 遥测数据
  7. php中解析数组,在PHP中解析多维数组
  8. Idea项目中常见错误及笔记(Old)
  9. phoenix 根据条件更新_元旦前不必扎堆买新车!北京:出售车辆后,申请更新指标无时限...
  10. 2100 没有反弹shell_反弹shell | ncbash
  11. linux ora-00322,ASM丢失disk header导致ORA-15032、ORA-15040、ORA-15042 Diskgroup无法mount
  12. 视觉的力量,如何利用视频和社交媒体讲述品牌故事
  13. Hive 3.x|第八天|DML函数
  14. #51CTO学院四周年#一路前行,一路陪伴
  15. 如何打开被关闭的任务管理器
  16. 用jq做一个点击图片放大消失
  17. 计算机信息安全及职业道德,CISP人员职业道德的准则
  18. 东西方赌王“口水战”升级 措词激烈论“竞争”
  19. 取消endnotes参考文献格式域的步骤_EndNote参考文献格式的更改(转载)
  20. 防止进程被任务管理器关掉的办法

热门文章

  1. linux 第十五天 linuxprobe
  2. 网页图标/images/favicon.ico type=image/x-icon /
  3. OpenCV笔记-对轮廓进行平滑处理
  4. 2019-12-24
  5. springboot权限系统
  6. 深入了解OpenOffice.org(三)
  7. Hadoop_GJF_MBY
  8. html翻牌动画效果,js+css3翻牌动画效果
  9. 神经性脚臭案例整理(三)
  10. Leetcode 1024 - 视频拼接