并发编程(一)| Volatile 与 Synchronized 深度解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 今天这篇是我的好朋友 evil say的投稿,这小伙现在大四,客观来说,大四有这个实力,我觉得很不错。他目前正在找实习,如果看了本文觉得他可以,有公司有坑位、愿意抛出橄榄枝的话。请联系他:hack7458@outlook.com

一、Volatile 关键字的实现及定义


1.1 定义


Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致性的更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁要更加的方便。如果一个字段被声明成 Volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的。


instance = new Sigleton; // instance 是 volatile 变量
// 转变成汇编代码
// 0x01a3deld: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0 x 0,(%esp)


1.2 实现


  1. Lock 前缀指令会引起处理器回写到内存Lock 信号确保在声言 (不达目的,不罢休) 该信号期间处理器可以独占任何内存,以前是锁总线(CPU 不能访问系统内存),但是在最近的处理器当中一般都是锁缓存,它会锁住这块内存区域的缓存并回写到内存当中,此操作叫缓存锁定,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。


  1. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效,处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器缓存的数据在总线上保持一致性,在下次访问相同内存地址的时候,强制执行缓存行填充


Lock 前缀指令:Lock 前缀指令在多核 CPU 下将当前处理器缓存行写回到系统内存,这个写回操作会使其他 CPU 缓存了该内存的地址的数据无效缓存行填充当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存 (L1,L2,L3 或所有)。


1.3 Volatile 是如何保证可见性的呢?


如果对声明了 Volatile 的变量进行写操作,JVM 会向处理器发送一条 Lock 前缀指令,将这个变量所在缓存行的数据写回到系统内存当中。在多处理器下,为了保证各个处理器缓存的缓存是一致的,就会实现缓存一致性协议当处理器发现自己缓存行对应的内存地址被修改,就会将当前的处理器缓存设置为无效,处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里缓存行CPU 高速缓存中可以分配的最小存储单位,处理器填写缓存行时会加载整个缓存行。缓存一致性协议缓存一致性协议通俗来讲是在多 CPU 的场景下,为了实现多线程同步而采取的一种技术手段,就像多线程同步是一种线程级别间的一致性保证。


特性


  1. 可见性:当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。


  1. 禁止指令重排序


禁止重排序: 编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。


缺点


  1. 在多线程中写多读少的情况下,使用 Volatile 会导致性能问题,及数据丢失问题。
  2. 对任意单个 Volatile 变量的读写具有原子性,但类似于 Volatile++ 复合操作不具备原子性


1.4 为什么 volatile++ 复合操作不具备原子性呢?


为了保证处理器中缓存一致性,会将当前的处理器缓存设置为无效的,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。注意的是这里的修改操作,是指的一个操作。可以知道自增操作是三个原子操作组合而成的复合操作。在一个操作中,读取了 inc 变量后,是不会再读取的 inc 的,所以它的值还是之前读的 10,它的下一步是自增操作。


二、Synchronized 关键字的实现及定义


2.1 定义


synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。JavaSE1.6 之后相继为 Synchronized 为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁轻量级锁


2.2 Synchronized 使用场景


  • 对于普通同步方法,锁是当前的实例对象
  • 对于静态同步方法,锁是当前类的 Class 对象
  • 对于同步方法块,锁是 Synchronized 括号里配置的对象


2.3 Synchronized 实现原理


synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor (monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁(这也是为什么 Java 中任意对象可以作为锁的原因)的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。


2.3.1 Java 对象头


在 JVM 存储大量存储对象同时,存储时为了实现一些额外的功能,需要在对象头添加一些标记字段用于增强对象功能,这些标记字段组成了对象头,Java 对象头分为数组类型跟非数组类型一个占用 4 字节,一个占用 8 字节。


2.3.2 Java 对象头的组成部分


  • Mark Word:存储对象的 hashcode 信息及锁信息等。
  • Class MeteData Address:存储对象类型数据的指针
  • ArrayLength: 数组的长度(如果对象是数组的话)


2.3.3 单例模式下的 Synchronized 的使用


//双重检查锁
public class Singleton(
    //使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
    private volatile static Singleton instance
    private Singleton()
    public Singleton getInstance(
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    )
)


2.3.4 偏向锁


在多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,为了让其获得锁的代价更低而引入了偏向锁。


  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需要简单测试一下对象头的 MarkWord 里是否存储着指向当前线程的偏向锁
  • 如果成功表示当前已经获得了锁
  • 如果没有设置则使用 CAS 竞争锁
  • 如果设置了尝试使用 CAS 将对象头的偏向锁指向当前线程


2.3.5 轻量级锁


引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过 CAS 竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。


  • 轻量级锁的执行过程:线程执行同步块的之前,如果同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储对象目前 MarkWord 的拷贝,然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针。如果成功,当前线程获得锁,并将 MarkWord 中存储锁的信息更新为 00,如果失败了说明当前锁存在竞争,锁就会膨胀成重量级锁随后更新 MarkWord 锁的信息为 10。


2.3.6 重量级锁


重量级锁通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。


2.4 锁的优缺点对比


优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗 CPU 追求响应时间,锁占用时间很短
重量级锁 线程竞争不使用自旋,不会消耗 CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长


三、简单讲讲 CAS


全称是 Compare and Swap,即比较并交换。是通过原子指令将获取存储在内存地址的原值和指定的内存地址进行比较,只有当它们值相等时,交换指定的预期值和内存中的值,这个操作是原子操作,若不相等,则重新获取存储在内存地址的原值。


3.1 CAS 流程?


CAS 是一种无锁算法,有 3 个关键操作数,内存地址,旧的内存中预期值,要更新的新值,当内存值和旧的内存中预期值相等时,将内存中的值更新为新值。


3.2 CAS 有什么弊端吗?


比较著名有 ABA 问题,当 CAS 在操作的时候会检查变量的值是 A,接着变成 B,最后又变成 A,实际上这个值已经是被修改过的,为了解决这个问题,JDK 中提供了 AtomicStampedReference 类解决 ABA 问题,用 Pair 这个内部类实现,包含两个属性,分别代表版本号和引用,在 compareAndSet 中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。


3.3 自旋锁与自适应自旋


线程的挂起和恢复会极大的影响开销,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置 10 次。这样就避免了线程切换的开销,极大的提升了性能。而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋 10 次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

相关文章
|
5月前
|
缓存 Java 调度
Java并发编程:深入解析线程池与Future任务
【7月更文挑战第9天】线程池和Future任务是Java并发编程中非常重要的概念。线程池通过重用线程减少了线程创建和销毁的开销,提高了资源利用率。而Future接口则提供了检查异步任务状态和获取任务结果的能力,使得异步编程更加灵活和强大。掌握这些概念,将有助于我们编写出更高效、更可靠的并发程序。
|
1月前
|
安全 测试技术 Go
Go语言中的并发编程模型解析####
在当今的软件开发领域,高效的并发处理能力是提升系统性能的关键。本文深入探讨了Go语言独特的并发编程模型——goroutines和channels,通过实例解析其工作原理、优势及最佳实践,旨在为开发者提供实用的Go语言并发编程指南。 ####
|
2月前
|
数据处理 Python
深入探索:Python中的并发编程新纪元——协程与异步函数解析
深入探索:Python中的并发编程新纪元——协程与异步函数解析
31 3
|
5月前
|
安全 Java 数据处理
Java并发编程:线程同步与协作的深度解析
在探索Java并发编程的海洋中,线程同步与协作的灯塔指引着航向。本文将深入挖掘线程同步机制的核心原理,揭示锁、条件变量等工具如何确保数据的一致性和线程间有序的通信。通过案例分析,我们将解码高效并发模式背后的设计哲学,并探讨现代Java并发库如何简化复杂的同步任务。跟随文章的步伐,您将获得提升多线程应用性能与可靠性的关键技能。 【7月更文挑战第24天】
50 5
|
5月前
|
数据处理 Python
深入探索:Python中的并发编程新纪元——协程与异步函数解析
【7月更文挑战第15天】Python 3.5+引入的协程和异步函数革新了并发编程。协程,轻量级线程,由程序控制切换,降低开销。异步函数是协程的高级形式,允许等待异步操作。通过`asyncio`库,如示例所示,能并发执行任务,提高I/O密集型任务效率,实现并发而非并行,优化CPU利用率。理解和掌握这些工具对于构建高效网络应用至关重要。
61 6
|
5月前
|
监控 Java API
Java并发编程之线程池深度解析
【7月更文挑战第14天】在Java并发编程领域,线程池是提升性能、管理资源的关键工具。本文将深入探讨线程池的核心概念、内部工作原理以及如何有效使用线程池来处理并发任务,旨在为读者提供一套完整的线程池使用和优化策略。
|
5月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
96 0
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
76 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
62 0

推荐镜像

更多
下一篇
DataWorks