读书笔记之《Java 并发编程的艺术》
一、多线程语义
阿姆达尔定律通过系统中并行化和串行化的比重来描述多处理器系统能获得的运算加速能力,摩尔定律则用于描述处理器晶管体数量与运行效率之间的发展关系。这两个定律的更替代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展过程。
实现线程主要有3种方式:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现。Java 线程在 JDK1.2 之前,是基于称为“绿色线程”(Green Threads)的用户线程实现,而在JDK 1.2 之后,线程模型替换为基于操作系统原生线程模型来实现。
线程调度指的是系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。协同式线程调度的多线程系统,线程的执行时间由线程本身来控制,线程执行完毕后,通知下一个线程执行,Lua 语言的“协同例程”就是这类实现;抢占式线程调度的多线程系统,线程的执行时间由系统来分配,线程的切换不由线程本身来实现,Java 语言就是这类实现。
Java 语言定义了 6 种线程状态:
- 新建(New)
- 运行(Runnable):可能正在执行,可能等待着 CPU 为它分配执行时间。
- 无限期等待(Waiting):不会被分配 CPU 执行时间,要等待被其他线程显式的唤醒(Object.wait()、Thread.join()、LockSupport.park())。
- 限期等待(Timed Waiting):不会被分配 CPU 执行时间,在一定时间后它们会被系统自动唤醒(Thread.sleep()、Object.wait(time)、Thread.join(time)、LockSupport.parkNanos()、LockSupport.parkUntil())。
- 阻塞(Blocked):程序等待进入同步区域,等待着获取到一个排他锁。
- 结束(Terminated)
不可变(Immutable)对象一定是线程安全的,“不可变”带来的安全性是最简单和最纯粹的。如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它是不可变的;如果共享数据是一个对象,那就需要保证对象的行为不会对其产生任何影响才行,而最简单的保证方式就是把带有状态的变量都声明为 final,比如 String、枚举类、Integer/Long/Double、BigInteger/BigDecimal 等。
即使是单核处理器也支持多线程执行代码,CPU 通过给每个线程分配 CPU 时间片来执行任务,当前任务执行一个时间片后会切换到下一个任务,所以 CPU 通过不停的切换线程执行。
并发执行如果没有达到一定的数量级,速度反而会比串行执行要慢。这是因为线程有创建和上下文切换的开销。
如何减少线程创建和上下文切换的开销?(“vmstat 1” 的 cs 参数,查看线程切换的次数)
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,比如 CAS 算法、比如将数据的ID按照 Hash 算法取模分段,不同的线程处理不同段的数据。
- 使用最少线程。避免创建不需要的线程,比如通过线程池等方式。
- 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。(java 本身并不支持协程,可以参考 Quasar)
如何避免死锁?
- 避免一个线程同时获取多个锁。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁(也称互斥锁)状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种策略的目的是为了提高获得锁和释放锁的效率。有意思的是除了偏向锁,JVM 实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环 CAS 的方式来获取锁,当它退出同步块的时候使用循环 CAS 释放锁。
如果只有一个线程进入同步代码块,那么它首先会获得“偏向锁”;当存在线程间竞争的时候,“偏向锁”会撤销,从而使用“轻量级锁”;当线程通过自旋方式始终获取不到“轻量级锁”时(获取锁的线程执行时间过长等原因),那么“轻量级锁”会膨胀成“重量级锁”。基本上所有的并发模式 在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。
二、Java 内存模型
JMM(Java 内存模型)采用共享内存模型,通过控制主内存(Main Memory)与每个线程的本地内存(Local Memory)之间的交互,来为 Java 程序员提供内存可见性保证。
从 Java 源代码到最终实际执行的指令序列,会分别经历“编译器优化的重排序”、“指令级并行的重排序”、“内存系统的重排序”,前一个属于编译器重排序,后两个属于处理器重排序,这些重排序会导致多线程程序出现内存可见性问题。为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障分为四类:
- 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。
- 对于不会改变程序执行结果的重排序,JMM 对编译器和处理器不做要求。
- 因此,在程序员看来,程序执行的语义(执行结果)不会改变,happens-before 关系本质上和 as-if-serial 语义是一回事。
happens-before 是 JMM 最核心的概念。happens-before 规则对应于一个或多个编译器和处理器重排序规则,JMM 通过 happens-before 规则隐藏了复杂的重排序规则以及这些规则的具体实现,而程序员在 happens-before 规则上编程,从而保证内存可见性。
- 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。这个规则由 as-if-serial 语义保证。
- 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
顺序一致性内存模型是一个理论参考模型,JMM 和处理器内存模型在设计时通常会以顺序一致性内存模型为参照。
顺序一致性模型、JMM、处理器内存模型,内存模型设计由强变弱,因为越是追求性能,内存模型就会设计的越弱,以此减少内存模型对它们的束缚。
三、synchronized、volatile和 final 的内存语义
理解 volatile 特性的一个好方法是把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。简而言之,volatile 变量自身具有下列特性:
- 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
- 原子性。对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
volatile 关键字如何保证可见性?volatile 的内存语义?
- 当对 volatile 变量写的时候,会将当前处理器缓存行的数据写回到系统内存。
- 当对 volatile 变量读的时候,会将当前处理器缓存行的数据置为无效,因此要从系统内存中读取变量值。
volatile 关键字如何保证有序性?
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止上面的普通写和下面的 volatile 写重排序)。
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(防止上面的 volatile 写与下面可能有的 volatile 读/写重排序)。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止下面所有的普通读操作和上面的 volatile 读重排序)。
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止下面所有的普通写操作和上面的 volatile 读重排序)。
为什么 JDK 文档说 CAS 同时具有 volatile 读和 volatile 写的内存语义?
- 编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序。
- 编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
- CAS(compare and swap)操作意味着要对 volatile 变量先读后写,要同时具备 volatile 读和写的语义,因此编译器不能对 CAS 与 CAS 前面和后面的任意内存操作重排序。
Synchonized 关键字的原理?JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明。
锁的释放和获取的内存语义?
- 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。(和 volatile 写的内存语义相同)
- 当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,因此要从系统内存中读取变量值。(和 volatile 读的内存语义相同)
对于 final 变量,编译器和处理器要遵守两个重排序规则(对象引用不在构造函数中“溢出”的情况下):
- 在构造函数内对一个 final 变量的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含 final 变量的对象引用,与随后初次读这个 final 变量,这两个操作之间不能重排序。
final 变量的内存语义?
- 写 final 变量的重排序规则会要求编译器在 final 变量的写之后,构造函数 return 之前插入一个 StoreStore 屏障。
- 读 final 变量的重排序规则会要求编译器在读 final 变量的操作前面插入一个 LoadLoad 屏障。
在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“溢出”。因为 JMM 无法保证在构造函数中“对变量的写”和“被构造对象的引用” 这两者之间是否会被重排序。
四、其他
内存屏障实际上就是一个 CPU 指令,插入一个内存屏障,相当于告诉 CPU 和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同 CPU 的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个 cpu 核心或者哪颗 CPU 执行的。
可以使用ODPS、Hadoop 或者自己搭建服务器集群来解决硬件资源限制的问题。
一个对象的引用占 4 个字节。
参考资料:
《Java 并发编程的艺术》
《深入理解 JVM 虚拟机》
读书笔记之《Java 并发编程的艺术》的更多相关文章
- 《HTML5与CSS3基础教程(第8版)》
<HTML5与CSS3基础教程(第8版)> 基本信息 原书名:HTML and CSS:visual quickstart guide 作者: (美)Elizabeth Castro ...
- HTML5与CSS3基础教程(第7版) 高清PDF扫描版
HTML5与CSS3基础教程(第7版)试读不仅介绍了文本.图像.链接.列表.表格.表单.多媒体等网页元素,也介绍了如何为网页设计结构.布局,添加动态效果.格式化等形式,此外还涉及调试和发布.聚合和吸引 ...
- HTML5与CSS3基础教程(第8版) PDF扫描版
<HTML5与CSS3基础教程(第8版)>自第1版至今,一直是讲解HTML和CSS入门知识的经典畅销书,全面系统地阐述HTML5和CSS3基础知识以及实际运用技术,通过大量实例深入浅出地分 ...
- 【02】HTML5与CSS3基础教程(第8版)(全)
[02]HTML5与CSS3基础教程(第8版)(全) 共392页. (魔芋:大体上扫了一遍.没有什么新东西,都是入门的一些基础知识.) 已看完. [美]elizabeth cast ...
- HTML5与CSS3基础教程第八版学习笔记11~15章
第十一章,用CSS进行布局 开始布局注意事项 1.内容与显示分离 2.布局方法:固定宽度和响应式布局 固定宽度,整个页面和每一栏都有基于像素的宽度 响应式布局也称为流式页面,使用百分数定义宽度 3.浏 ...
- HTML5和CSS3基础教程(第8版)-读书笔记(3)
第11章 用CSS 进行布局 网站设计主要有两大类型:固定宽度和响应式. 对于固定(fixed)布局,整个页面和每一栏都有基于像素的宽度.顾名思义,无论是使用移动电话和平板电脑等较小的设备查看页面,还 ...
- HTML5和CSS3基础教程(第8版)-读书笔记(2)
第7章 CSS构造模块 7.1 构造样式规则 样式表中包含了定义网页外观的规则.样式表中的每条规则都有两个主要部分:选 择 器(selector) 和 声 明 块(declaration block) ...
- HTML5和CSS3基础教程(第8版)-读书笔记
第1章 网页的构造块 一个网页主要包括以下三个部分: n 文本内容(text content):在页面上让访问者了解页面内容的纯文字. n 对其他文件的引用(referen ...
- HTML5和CSS3基础教程(第8版)-读书笔记(4)
第16章 表单 表单有两个基本组成部分:访问者在页面上可以看见并填写的控件.标签和按钮的集合:以及用于获取信息并将其转化为可以读取或计算的格式的处理脚本. 基本的表单字段类型包括文本框.单选按钮.复选 ...
- 读书笔记之《HTML5 与 CSS3 基础教程》
1· 读前预期 考虑到对于 Web 开发零基础,凡涉足一件未知的任务,最好先理清任务的逻辑结构,然后有目的地逐步学习.为实现我们的需求和设计,必须要学习前端.后端.服务器等一系列暂时陌生的知识,在此, ...
随机推荐
- input监听
<h1> 实时监测input中值的变化 </h1> <input type="text" id="username" autoco ...
- 高德APP全链路源码依赖分析工程
一.背景 高德 App 经过多年的发展,其代码量已达到数百万行级别,支撑了高德地图复杂的业务功能.但与此同时,随着团队的扩张和业务的复杂化,越来越碎片化的代码以及代码之间复杂的依赖关系带来诸多维护性问 ...
- 转:<context:component-scan>使用说明
在xml配置了这个标签后,spring可以自动去扫描base-pack下面或者子包下面的java文件,如果扫描到有@Component @Controller@Service等这些注解的类,则把这些类 ...
- redis--linux环境搭建
1.redis诞生的背景 在这要从08年开始说起,一个意大利的小伙子创建一个访问网站信息的LLOOGG.COM网站,用来记录网站的访问记录,查看最近一万条访问信息,每次访问都会将数据存入mysql当中 ...
- play-with-docker搭配ffsend完成文件上传及下载(解决从docker hub拉取镜像慢问题)
由于众所周知的原因,大家有的时候pull docker hub上的镜像是很困难的,下载到99%就这么不动了也是很正常的事情 这个时候以下步骤是100%可以解决问题的: 1.找一台国外的服务器安装doc ...
- 【实用工具】使用Java封装可执行exe应用全过程
目录 编写java代码 打包 创建exe文件 压缩 总结 本文将使用exe4j将java项目封装为可以发送给他人使用的工具为例,来记录将java项目封装为exe文件的全过程 编写java代码 目标:创 ...
- NRF52810和NRF52832的区别
NRF52832和NRF52810都是蓝牙5.0的芯片 均是出至NORDIC. 主要区别是 1.NRF52810的Flash是192KB/ RAM是24KB NRF52832的Flash是512 ...
- Springboot结合Redis
安装 Redis 安装 gcc Yum install gcc-c++ 解压 redis.3.0.0.tar.gz 压缩包 tar -zxvf redis-3.0.0.tar.gz 进入解压后的目 ...
- ThreadLocal的进化——TransmittableThreadLocal
上一篇文章中,我们谈到了 InheritableThreadLocal,它解决了 ThreadLocal 针对父子线程无法共享上下文的问题.但我们可能听说过阿里的开源产品TransmittableTh ...
- ARTS-S docker ceontos镜像中使用crontab
centos镜像中默认没有crontab,需要在dockerflle中通过yum的安装 yum -y install vixie-cron crontabs && yum clean ...