软硬件发展概述
Amdahl定律和摩尔定律
1)Amdahl定律:通过系统中并行化和串行化的比重
来描述多处理器系统能获得的运算加速能力
。
2)摩尔定律:用于描述处理器晶体管数量
与运行效率
之间的发展关系。
多任务处理的需求
计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O
、网络通信
或者数据库访问
上,为了避免处理器大部分时间处于等待其他资源的状态而浪费,就需要计算机能够同时处理多个任务
。
每秒事务处理数(TPS)
每秒事务处理数
(TPS,Transactions Per Second)代表一秒内
服务端平均能响应的请求总数
,TPS与程序的并发能力
强相关,我们一般就是通过TPS衡量一个服务性能好坏
。
高速缓存(Cache)的出现
高速缓存为什么出现?:由于计算机的存储设备(慢)
与处理器的运算速度(快)
有几个数量级的差
距,现代计算机系统需要加入一层读写速度
尽可能接近处理器运算速度
的高速缓存
作为内存与处理器之间的缓冲
。
高速缓存的原理:将运算需要使用的数据复制到缓存
中,让运算能快速进行,当运算结束后,再从缓存中同步回内存
之中,这样处理器就无须等待缓慢的内存读写了。
引入的问题:基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是为计算机带来更高的复杂度,如缓存一致性问题
。
缓存一致性问题
介绍:在多处理器系统中,每个处理器都有自己的高速缓存
,它们之间又共享同一主内存
,当多个处理器的运算任务都涉及同一块主内存时,将可能导致各自的缓存数据不一致,则同步到主内存时数据不统一。
解决方案:各个处理器访问缓存时遵守一些缓存一致性协议
。
内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
Java内存模型JMM
JMM介绍
Java内存模型(JMM,Java Memory Model)屏蔽各种硬件和操作系统的内存访问差异
,实现让Java程序在各种平台下能达到一致的内存访问效果
。目标是定义程序中各个变量的访问规则
,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
变量
JMM中的变量是指实例字段
、静态字段
和构成数组对象的元素
,不包括局部变量与方法参数,因为是线程私有的,不被共享,则不存在竞争。
注意:若局部变量是一个reference类型,即对象引用类型,reference引用的对象是在Java堆中,可以被各个线程所共享,但是reference本身在Java栈中的局部变量表中,是线程私有的。
主内存和工作内存(JMM模型)
主内存:
- Java内存模型规定
所有变量
(实例字段、静态字段和构成数组对象的元素)都存储在主内存
(Main Memory)中。 - 若要对应Java内存区域,主内存则对应
Java堆中的对象实例部分
。
工作内存:
每条线程
都有自己的工作内存
,线程的工作内存保存了被线程使用到的变量的主内存副本拷贝
。线程对变量的所有操作
(读取、赋值)都必须在工作内存中进行
,而不能直接读写主内存
中的变量。- 不同的线程
之间无法直接访问对方
的工作内存
中的变量,线程间变量值的传递
必须通过主内存
来完成。(主内存相当于一个中转站
); - 若要对应Java内存区域,则工作内存对应
Java虚拟机栈
中的部分区域。
主内存和工作内存交互操作(8个)
操作 | 作用区域 | 作用 |
---|---|---|
lock (锁定) |
主内存变量 | 把一个变量标识为一条线程独占的状态。 |
unlock (解锁) |
主内存变量 | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 |
read (读取) |
主内存变量 | 把一个变量的值从主内存传输到线程的工作内存中,便于load的使用。 |
load (载入) |
工作内存变量 | 把read操作从主内存得到的变量值放入工作内存的变量副本中。 |
use (使用) |
工作内存变量 | 把工作内存中的一个变量值传递给执行引擎,每当VM遇到一个需要使用变量值的字节码指令时执行。 |
assign (赋值) |
工作内存变量 | 把一个从执行引擎接收到的值赋给工作内存的变量,每当VM遇到一个给变量赋值的字节码指令时执行。 |
store (存储) |
工作内存变量 | 把工作内存中的一个变量值传送到主内存中,便于write的使用。 |
write (写入) |
主内存变量 | 把store操作从工作内存得到的变量值放入主内存的变量中。 |
1)从主内存复制变量到工作内存:顺序执行read和load操作;可以不连续
2)从工作内存同步变量到主内存:顺序执行store和write操作;可以不连续
3)操作规则8个:
不允许read和load
、store和write
操作之一单独出现
:不允许发生一个变量从主内存读但工作内存不接受(只有read却没有load),或从工作内存回写但主内存不接受(只有store却没有write);不允许
一个线程丢弃
它的最近的assign
操作:变量在工作内存中改变之后必须
把该变化同步回主内存
;(在工作内存中变量值的改变需要之后刷新到主内存中)不允许
一个线程无原因
的把数据同步回主内存
中:没有发生任何的assign操作;(必须要有变量值的改变
,才能刷新到主内存)- 一个
新的变量只能
在主内存中产生
,不能在工作内存中直接使用(use或者store操作)一个未被初始化(load或assign)的变量:对一个变量实施use或store之前,必须先执行load或assign操作。 - 一个变量在
同一个时刻只能
被一个线程
对其进行lock
操作,但lock操作可以被同一条线程执行多次
,多次执行lock后, 只有执行相同次数的unlock
操作,变量才能释放
; - 如果对一个变量
执行lock
操作,则会清空工作内存中这个变量的值
,在执行引擎使用该变量前,需要重新执行load或assign初始化变量的值; - 若一个变量事先
没有被lock
,则不允许对其unlock
,也不允许去unlock一个被其他线程锁定的变量:一个lock对应一个unlock
; - 对一个变量
执行unlock之前
, 必须要把这个变量同步回主内存
中(store、write操作)。
原子性、可见性和有序性
原子性
Atomicity原子性
:指一个操作或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。由Java内存模型直接保证的原子性变量操作包括:read、load、use、assign、store和write,基本数据类型的访问读写是具有原子性的(long和double64位的非原子性协定)- 若要更大的原子性保证需要
lock
和unlock
操作,通过字节码指令monitorenter
和monitorexit
隐式使用这两个操作,同步块—synchronized
关键字的使用。
补充:long和double 的非原子性协定
允许虚拟机将没有被volatile修饰的64位数据的读写
操作划分为两次32位
的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的read、load、store和write这4个操作的原子性。在编写代码时不需要把long和double 变量
专门声明成volatile
的。
可见性
Visibility可见性
:是指当一个线程修改共享变量的值,其他线程能够立即得知这个修改。- JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种
依赖主内存作为传递媒介
来实现可见性。 - 普通变量或者volatile变量都是依赖主内存作为传递媒介来实现可见性,只不过
volatile变量
是新值是立即同步到主内存
到且每次读取前是立即从主内存刷新
(底层通过内存屏障
来实现,内存屏障可以保证其他线程
的缓存行无效
,只能去主内存刷新
读取新值)。 - 除了volatile实现可见性,synchronized和final也可以实现可见性。
synchronized实现可见性
,即同步块的可见性是“对一个变量unlock前,必须先把此变量同步回主内存中
(store、write);final可见性
是指“被final修饰的字段在构造器中一旦初始化完成
,且构造器没有把”this“的引用传递出去,则其他线程就可以看到final字段
的值。”
有序性
Ordering有序性
:指程序执行的顺序按照代码的先后顺序执行。- 如果在
本线程内观察
,所有的操作都是有序
的,这是相城内表现出串行的语义;如果在一个线程观察另一个线程
,则所有的操作都是无序
的,这是指令重排序
及工作内存和主内存同步延迟效果
。 - 通过
volatile
和synchronized
关键字保证线程之间
的有序性
:volatile禁止指令重排序
;synchronized
是一个变量
在同一个时刻只允许一条线程
对其lock
操作,即持有同一个锁
的两个同步块
只能串行
地进入。 - 处理器在进行重排序时会考虑指令之间的
数据依赖性
,如果一个指令a必须用到另一条指令b的结果,则处理器会保证指令b在指令a之前执行,只会对不存在数据依赖性的指令
进行重排序
。 - 指令重排序不会影响单个线程的执行,但是会影响到线性并发执行的正确性。
先行发生原则(happens-before)
介绍
先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生原则是JMM定义
的两个操作之间
的偏序关系
。如操作A先行发生于操作B,则在发生操作B之前,操作A的影响(修改内存中共享变量的值、发生消息、调用方法)会被操作B观察到。这些影响包括了修改内存中共享变量的值、发送消息、调用方法等
。
示例
场景一:
线程a:
i = 10;
线程b:
j = i;
我们看上面的操作,线程a的操作i = 10
是发生在线程b的操作j = i
之前的,线程a的i = 10
结果可以被线程b观察到,且无其他线程操作该i值,所以线程b执行完操作后,j的结果是10。
场景二:
线程a:
i = 10;
线程c:
//不确定执行时机
i = 20;
线程b:
j = i;
在场景二中,除了线程a和线程b,出现了线程c,虽然线程a和线程b是保持了相信发生关系,但是线程c和线程b没有先行发生关系,所以,j的值就无法确定。会出现两种情况:1)线程a—线程c—线程b
执行顺序,则j的值为20;2)线程a—线程b—线程c
执行顺序,则j的值为10。
规则
以下的规则都是无需同步器协助就已天然存在的。
规则 | 介绍 |
---|---|
程序次序规则 |
在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(控制流顺序,如分支、循环等结构) |
管程锁定规则 |
一个unlock先行发生于后面对同一个锁的lock操作。 |
volatile变量规则 |
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。 |
线程启动规则 |
Thread对象的start()方法先行发生于此线程的每一个动作。 |
线程终止规则 |
线程中的所有操作都先行发生于对此线程的终止检测。通过Thread.join() 方法结束、Thread.isAlive() 的返回值检测到线程已经终止。 |
线程中断规则 |
对线程interrupt() 方法的调用先行发生于被中断的代码检测到中断事件的发生,可通过Thread.interrupted() 方法检测到是否有中断发生。 |
对象终结规则 |
一个对象的初始化完成(构造函数执行结束)先行发生于finalize() 方法的开始。 |
传递性 |
操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。 |
对于setter/getter方法读写操作实现先行发生原则
方案一:把setter/getter方法都定义为synchronized方法
,套用管程锁定规则
。
方法二:把setter/getter中共享变量定义为volatile变量
,而setter中的变量不依赖于原值,所以满足volatile的使用场景,可以套用volatile变量规则
来实现先行发生关系。
参考书籍
《深入理解Java虚拟机:JVM高级特性及最佳实践》