【并发编程的艺术】并发机制原理
java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化成汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令
更好的进行并发编程,需要深入了解Java并发机制的底层实现原理
一、volatile的应用
在多线程并发编程中synchronized
和volatile
都扮演着重要的角色
volatile
是轻量的synchronized
,它在多处理开发中保证了共享变量的可见性。可见性指的是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
如果volatile
变量修饰符使用恰当的话,它比synchronized
的使用和执行成本更低,因为它不会引起线程上下文切换和调度
接下来深入分析在硬件层面上Intel
处理器是如何实现volatile
,通过深入分析可以更好的使用volatile变量
1.1 volatile的定义与实现原理
volatile的定义:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量
volatile在某些情况下比锁要更加方便,如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值时一致的
在了解volatile实现原理前,首先了解一下与其实现原理相关的CPU的CPU术语与说明
内存屏障(memory barries)
一组处理器指令,用于实现对内存操作的顺序限制缓冲行(cache line)
CPU高速缓存中可以分配的最小存储单元
处理器填写缓存行时会加载整个缓存行,现代CPU需要执行几百次CPU指令原子操作(atomic operations)
不可中断的一个或一系列操作缓存行填充(cache line fill)
当处理器识别到内存中读取器操作数时可缓存的,处理器读取整个高速缓存行到适当的缓存(L1、L2、L3或所有)缓存命中(cache hit)
如果进行高速缓存行填充操作的内存位置仍然时时下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取写命中(write hit)
当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中写缺失(write misses the cache)
一个有效的缓存行写入到不存在的内存区域
1.2 volatile如何保证可见性?
在x86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情
Java代码:
instance = new Singleton(); // instance 是 volatile变量
转变成汇编代码:
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码
lock前缀的指令在多核处理器下会引发的两件事情
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存来该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内存缓存(L1、L2或其他)后再进行操作,但是操作完全不知道何时会写到内存
Lock前缀
如果对声明来volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存
缓存一致性
但是就算写回到内存,如果其他处理器缓存的值还是旧的值的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了
缓存重置
当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
二、synchronized的应用
在多线程并发编程中,synchronized
一直都是元老级角色,也会被称为重量级锁
接下来介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程
2.1 利用synchronized实现同步锁的基础
Java中的每一个对象都可以作为锁,具体表现形式:
对于普通同步方法,锁是当前实例对象
对于静态同步方法,锁是当前类的Class对象
对于同步方法块,锁是Synchronized
括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁
2.2 锁存储的信息
那么锁到底存在哪里呢?锁里面会存储什么信息呢?
JVM基于进入和退出Monitor
对象来实现方法同步和代码块同步,但两者的实现细节不一样
代码块同步
使用monitorenter
和monitorexit
指令实现的方法同步
使用另外一种方式实现的,虽然JVM规范中没有详细说明,但是方法同样可以使用这两个指令来实现
monitorenter指令: 在编译后插入到同步代码块的开始位置
monitorexit指令: 在编译后插入到方法结束处和异常处
JVM要保证每个monitorenter
必须有对应的monitorexit
与之配对
任何一个对象都有一个monitor
与之关联,当且一个monitor
被持有后,它将处于锁定状态
线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor
所有权,即尝试获得对象的锁
2.3 锁的升级与对比
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了"偏向锁"和"轻量级锁"
锁一共有4中状态,级别从低到高依次是:
无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
这几个状态会随着竞争情况逐渐升级,且锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁
这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
当一个线程访问同步块并获取锁,会在对象头和栈帧中的锁记录里存储锁偏向线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
如果测试成功
表示线程已经获得了锁如果测试失败
则需要再测试一下Mark Word中偏向锁的标识是否设置成了1(表示当前是偏向锁):
如果没有设置,则使用CAS竞争锁
如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程
轻量级锁
轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象中的Mark Word替换为指向锁记录的指针
如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头
如果成功,则表示没有竞争发生
如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态
当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争
2.4 原子操作的实现原理
处理器如何实现原子操作
基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作
处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性
使用总线锁保证原子性
如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致
处理器使用总线锁就是解决这个问题,所谓总线锁就是使用处理器提供的一个LOCK #信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存使用缓存锁保证原子性
同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁替代总线锁来进行优化
什么是缓存锁
缓存锁指的是内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK #信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性(因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效)
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域
JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁
有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式获取锁,当它退出同步块的时候使用循环CAS释放锁
【并发编程的艺术】并发机制原理相关推荐
- Java并发编程的艺术-并发编程基础
Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势.线程作为操作系统调度的最小单元,多个线程能够同时执行,这将显著提升程序性能,在多核环境中表现 ...
- java future_Java并发编程之异步Future机制的原理和实现
Java并发编程之异步Future机制的原理和实现 项目中经常有些任务需要异步(提交到线程池中)去执行,而主线程往往需要知道异步执行产生的结果,这时我们要怎么做呢?用runnable是无法实现的,我们 ...
- 多线程知识梳理(2) - 并发编程的艺术笔记
layout: post title: <Java并发编程的艺术>笔记 categories: Java excerpt: The Art of Java Concurrency Prog ...
- 《Java并发编程的艺术》笔记
<Java并发编程的艺术>笔记 第1章 并发编程的挑战 1.1 上下文切换 CPU通过时间片分配算法来循环执行任务,任务从保存到再加载的过程就是一次上下文切换. 减少上下文切换的方法有4种 ...
- 并发编程的艺术 读书笔记
第一章 并发编程的挑战 1. 单核CPU分配运行时间给各个线程,实现多线程执行代码. 举例:看英文书时某个单词不会,先记住看到书的页数和行数,然后去查单词,查完回到看书状态,相当于上下文切换. 2. ...
- Java并发编程的艺术(一)
看<java并发编程的艺术>这本书,想着看的时候做个简单的总结,方便以后直接看重点. 一.并发编程的挑战 1.上下文切换 Cpu时间片通过给每个线程分配CPU时间片来实现多线程机制,时间片 ...
- Java并发编程的艺术(推荐指数:☆☆☆☆☆☆)
文章目录 Java并发编程的艺术(推荐指数:☆☆☆☆☆☆) 并发编程的挑战 Java并发机制的底层实现原理 Volatile的应用 实现原理 synchronized的实现原理与应用 对象头 锁详解 ...
- # Java 并发编程的艺术(二)
Java 并发编程的艺术(二) 文章目录 Java 并发编程的艺术(二) 并发编程的挑战 上下文切换 如何减少上下文的切换 死锁 资源限制的挑战 Java 并发机制的底层实现原理 volatile 的 ...
- 《Java并发编程的艺术》——Java中的并发工具类、线程池、Execute框架(笔记)
文章目录 八.Java中的并发工具类 8.1 等待多线程完成的CountDownLatch 8.2 同步屏障CyclicBarrier 8.2.1 CyclicBarrier简介 8.2.2 Cycl ...
- 《Java并发编程的艺术》——Java并发的前置知识(笔记)
文章目录 一.并发编程的挑战 1.1 上下文切换 1.1.1 多线程一定快吗 1.1.2 如何减少上下文的切换 1.2 死锁 死锁发生的条件 预防死锁 避免死锁 1.3 资源限制的挑战 1.3.1 什 ...
最新文章
- 如何访问webService接口
- PHP并发验证,PHP接口并发测试的方法(推荐)
- Python基础——for/while循环
- 安卓开发30:AsyncTask的用法
- 【拔刀吧少年】之shell编程规范与变量
- @开发者 区块链技术如此火爆 你却只能望而却步?京东云为你配齐装备!
- Spring Boot 2.0.3 使用外置 Tomcat 服务器
- 《JAVA并发编程实践》读书笔记(一)
- PPT文件怎么快速压缩?
- iOS6.1完美越狱教程 一键越狱5分钟搞定
- Windows 2008 Server线程池前瞻
- ES学习构建EKL海量日志分析平台
- 阿里iDST NLP负责人司罗:NLP技术怎样一路走到阿里云
- python实现牛顿法_牛顿法和最速下降法的Python实现
- Fence Repair-栅栏维修(优先队列)
- 【Linux】/etc/issue、/etc/issue.net和/etc/motd的区别
- 通过Debug命令行清除BIOS Setup密码
- 爬取链家二手挂单房屋 - 匹配百度地图API坐标 - python出地图【2】(end) echarts调用百度地图画自己喜欢的底图,最终python出地图
- JAVA 类的继承(私有属性、自动转型)(入门级小白一看就懂)
- 微软 theme 主题文件官方文档中文翻译版