并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因
衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS)是最重要的指标之一
问题引出:
解决:各个处理器访问缓存时都遵循一些协议
内存模型:可以理解为特定的操作协议下,对特定的内存或高速缓存进行读写访问过程的抽象
乱序执行优化:
目的:为了使得处理器内部的运算单元能尽量被充分利用
过程:处理器可能会对输入代码进行乱序执行(Out-Of—Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码一致
引出:与之类似的是Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)
Java内存模型的主要目标:定义程序中各个变量的访问规则
即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数这种线程私有的变量
操作过程
目的:
内容概述:
具体操作(8种操作):
序号 | 名称 | 操作区域 | 作用 |
---|---|---|---|
1 | lock(锁定) | 作用于主内存的变量 | 把一个变量标识为一条线程独占的状态 |
2 | unlock(解锁) | 作用于主内存的变量 | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |
3 | read(读取) | 作用于主内存的变量 | 把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用 |
4 | load(载入) | 作用于工作内存的变量 | 把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
5 | use(使用) | 作用于工作内存的变量 | 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作 |
6 | assign(赋值) | 作用于工作内存的变量 | 把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
7 | store(存储) | 作用于工作内存的变量 | 把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用 |
8 | write(写入) | 作用于主内存的变量 | 把 store 操作从工作内存中得到的变量的值放入主内存的变量中 |
要求:
(1)每一种操作都是原子的、不可再分的
(2)如果要把一个变量从主内存复制到工作内存,那就要顺序的执行 read 和 load 操作,如果要把变量从工作内存同步回主内存,就要顺序的执行 store 和 write 操作
(3)不允许 read 和 load、store 和 write 操作之一单独出现
(4)不允许一个线程丢弃它的最近的 assign 操作
(5)不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
(6)一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量
(7)一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁
(8)如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值
(9)如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量
(10)对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)
概述:
volatile
关键字是Java 虚拟机提供的最轻量级的同步机制volatile
变量的两种特性(两种语义)
(1)保证此变量对所有线程的可见性
“可见性” 是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
普通变量的值在线程间传递则需要通过主内存来完成,做不到可见性。eg:线程A修改一个普通变量的值,然后向主内存回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见
(2)禁止指令重排序优化(JDK1.5才被完全修复实现此功能)
指令重排序定义:CPU采用了允许将多条指令不按照程序规定的顺序分开发送给各相应的电路单元处理。
普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致
因为在一个线程的方法执行过程中无法感知到指令重排序,这也就是Java内存模型中描述的所谓的 “线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)
volate
变量的并发安全性
语句 | 判断 |
---|---|
volatile 变量在各个线程中是一致的 |
正确 |
基于volatile 变量的运算在并发下是安全的 |
错误 |
volatile
变量在各个线程的工作内存中不存在一致性问题
volatile
变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的
仍需加锁的情况
由于volatile
变量只能保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(synchronized
或java.util.concurrent
中的原子类)来保证原子性(原因:一条字节码指令可能被转化成若干条本地机器码指令)
原理(以DCL单例为例):
有volatile
关键字修饰的变量,赋值后多执行了一个“lock addl $0x0, (%esp)
”操作,这个操作相当于是一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置)
指令重排序过程要求:CPU需要能正确处理指令依赖情况(eg:运算法则的先后顺序)以确保程序能得出正确的执行结果。所以在本内CPU内,重排序看起来依然是有序的。
因此通过“lock addl $0x0, (%esp)
”指令实现上述要求,将修改同步到内存时,说明其之前的操作(即被依赖的操作)都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果
只有一个CPU访问内存时不需要内存屏障。如果有两个或更多CPU访问同一块内存,并且其中又一个在观测另一个,就需要内存屏障来保证一致性
lock
前缀作用:使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache(其他CPU的数据失效)
volatile
变量的修改对其它CPU立即可见选择volatile
还是锁实现同步
volatile
是否能满足使用场景的要求volatile
的同步机制性能确实优于锁,volatile
变量的读操作的性能消耗与普通变量也几乎没有什么区别。但写操作可能会慢一些,因为需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行Java内存模型对volatile
变量定义的特殊规则
前提:假定T表示一个线程,V和W分别表示两个volatile
型变量,那么在进行 read、load、use、assign、store和write操作时需要满足如下规则
(1)read-load-use
必须连续配对出现
只有当线程T对变量V执行的前一个动作是 load 的时候,线程T才能对变量V执行 use 动作;并且,只有当线程T对变量V执行的后一个动作是 use 的时候,线程T才能对变量V执行 load 动作
这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于确保能看见其他线程对变量V所做的修改后的值
(2)assign-store-write
必须连续配对出现
只有当线程T对变量V执行的前一个动作是 assign 的时候,线程T才能对变量V执行 store 动作;并且,只有当线程T对变量V执行的后一个动作是 store 的时候,线程T才能对变量V执行 assign 动作
这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于确保其他线程可以看到自己对变量V的修改
(3)如果对V的 use 或 assign 操作先于对W的 use 或 assign 操作,则V的操作的连续配对操作(read、use或store、write)也先于W操作的连续配对操作
volatile
修饰的变量不会被指令重排序优化,确保代码的执行顺序与程序的顺序相同long和double的非原子性协定(Nonatomic Treatment of double and long Variables)
Java内存模型中的八种操作具有原子性,但对于64位数据类型(long、double)相对宽松:允许虚拟机将没有被volatile
修饰的64位数据的读写操作划分为两次32位的操作来进行
实际开发中,目前各个平台的商用虚拟机一般都把64位数据的读写操作作为原子操作来对待
volatile
(1)原子性(Atomicity)
基本数据类型的访问读写是具备原子性的(long、double除外)
在synchronized
块之间的操作实现更大范围的原子性
synchronized
-> 字节码指令中的monitorenter
和monitorexit
-> Java内存模型中的lock
和unlock
(2)可见性(Visibility)
定义:可见性是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改
实现原理:Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的
具体Java实现:
1⃣ volatile
变量:volatile
的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新(volatile
变量保证了多线程操作时变量的可见性)
2⃣ synchronized
同步块:同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)”这条规则获得的
3⃣ final
关键字:被final
修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸使其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final
字段的值
(3)有序性(Ordering)
定义:
如果在本线程中观察,所有的操作都是有序的:线程内表现为串行的语义
如果在一个线程中观察另一个线程,所有的操作都是无序的:“指令重排序”现象和“工作内存与主内存同步延迟”现象
实现:
volatile
关键字:有禁止指令重排序的语义synchronized
同步块:“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”,因此持有同一个锁的两个同步块只能串行的进入作用:是判断数据是否存在竞争、线程是否安全的主要依据
volatile
和synchronized
完成定义:
先行发生是Java内存模型汇总定义的两项操作之间的偏序关系,即“操作A先行发生于操作B”意味着发生操作B之前,操作A产生的“影响”能被操作B观察到
Java内存模型下的一些“天然的”先行发生关系:
概述:这些线程先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意的重排序。
(1)程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
(2)管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于(时间顺序)后面对同一个锁的 lock 操作
(3)volatile变量规则(Volatile Variable Rule):对一个volatile
变量的写操作先行发生于(时间顺序)后面对这个变量的读操作
(4)线程启动规则(Thread Start Rule):Thread对象的start()
方法先行发生于此线程的每一个动作
(5)线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测
Thread.join()
方法结束、Thread.isAlive()
的返回值等手段检测到线程已经终止执行(6)线程中断规则(Thread Interruption Rule):对线程interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生
Thread.interrupted()
方法检测到是否有中断发生(7)对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()
方法的开始
(8)传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
衡量并发安全问题,时间顺序不具有参考性(指令重排序等问题),一切必须以先行发生原则为准
(1)使用内核线程实现(一对一的线程模型)
定义:内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核(Kernel)支持的线程
特点:这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上
多线程内核:每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)
一对一的线程模型
轻量级进程优点:
轻量级进程缺点:
1⃣ 由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换
2⃣ 每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
(2)使用用户线程实现(一对多的线程模型)
(3)使用用户线程加轻量级进程混合实现(多对多的线程模型)
Java线程的实现
在JDK1.2前:基于称为“绿色线程”(Green Threads)的用户线程实现
在JDK1.2后:基于操作系统原生线程模型实现
因此在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的
对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的
定义:线程调度是指系统为线程分配处理器使用权的过程
(1)协同式线程调度(Cooperative Threads-Scheduling)
过程:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上
优点:
缺点:
(2)抢占式线程调度(Preemptive Threads-Scheduling)
过程:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
Thread.yield()
可以让出执行时间优点(Java使用此种调度方式):
Java实现中的优先级:
Java线程调度是系统自动完成的,但可通过语言中预设的10个线程优先级来增大线程被系统选择执行的概率
线程优先级不完全准确:Java的线程是通过映射到系统的原生线程上来实现的,所以线程的调度最终还是取决于操作系统。不同操作系统中的线程优先级不一定能与Java线程优先级一一对应,并且还存在一些特殊情况(eg:Windows系统中的优先级推进器可能会越过线程优先级给活跃线程分配执行时间)
概述:Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态
(1)新建(New):创建后尚未启动的线程处于这种状态
(2)运行(Runnable):Runnable 包括了操作系统线程状态中的 Running 和 Ready ,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间
(3)等待状态
1⃣ 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期等待状态:
没有设置Timeout参数的Object.wait()
方法
没有设置Timeout参数的Thread.join()
方法
LockSupport.park()
方法
2⃣ 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其他线程显式的唤醒,在一定时间之后他们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
Thread.sleep()
方法
设置了Timeout参数的Object.wait()
方法
设置了Timeout参数的Thread.join()
方法
LockSupport.parkNanos()
方法
LockSupport.parkUntil()
方法
(4)阻塞(Blocked):线程被阻塞。在程序等待进入同步区域的时候,线程将进入这种状态
“阻塞状态”在等待着获取一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生
“等待状态”在等待一段时间或是唤醒动作的发生
(5)结束(Terminated):已终止线程的线程状态,线程已结束执行