原理篇
1、 引入的问题
a) 共享资源的竞争--显示同步
b) 线程之间的通信—共享内存
2、 jvm内存模型
a) 引入的原因
ü 现代计算机都采用基于高速缓存的存储交互来解决处理器和内存的速度矛盾,但也引入了新的问题:缓存一致性
ü 为了使得处理器内部的运算单元尽可能被充分利用,处理器可能会对输入代码进行乱序执行优化,但会保证结果与顺序执行的结果一致(单线程下)
b) 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即虚拟机将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
Java内存模型中规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。程、主内存和工作内存的交互关系如下图所示:
c) 内存间的交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
ü lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
ü unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
ü read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
ü load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
ü use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
ü assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
ü store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
ü write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
l 不允许read和load、store和write操作之一单独出现
l 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
l 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
l 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,b必须先执行过了assign和load操作。
l 一个变量在同一时刻只允许一条线成对其进行lock操作,lock和unlock必须成对出现
l 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
l 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
l 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
d) volatile型变量的特殊规则
当一个变量被定义volatile之后,它将具备两种特性:
ü 保证此变量对所有线程的可见性。
ü 禁止指令重排序优化
假定T表示一个线程,V和W表示两个volatile型变量
ü 线程T对变量V执行use操作之前必须先执行load操作,load之后必须得use(该规则要求在工作内存中,每次使用变量V之前,必须先从主内存加载最新的值)
ü 线程T对变量V执行save操作之前必须先执行assign操作,assign之后必须得save(该规则要求在工作内存中,每次修改变量V之后,必须将最新值同步回主内存)
ü 假定动作A是线程T对变量V实施的use或assign操作,动作F是和动作A相关联的load或save操作,动作P是和动作F相关联的read或write操作;类似的假定动作B是线程T对变量W实施的use或assign操作,动作G是和动作B相关联的load或save操作,动作Q是和动作G相关联的read或write操作;如果A先于B,那么P先于Q(该规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)????????
e) 先行发生原则
先行发生是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被B观察到。
是判断数据是否存在竞争、线程是否安全的主要依据。
Java内存模型中天然的先行发生关系:
ü 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
ü 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
ü volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
ü 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
ü 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
ü 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断
ü 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
ü 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。
f) 数据依赖性
如果两个操作访问同一个变量,且有一个操作是写,此时两个操作之间就存在数据依赖性,处理器不会改变存在数据依赖关系的两个操作的执行顺序
3、 Java与线程
a) 线程的调度
线程调度是指系统为线程分配使用权的过程,主要有两种方式,协同式线程调度和抢占式线程调度,java采用抢占式线程调度
b) 线程状态
c) 线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
d) 线程安全的实现方法
ü 互斥同步
线程阻塞和唤醒耗性能
属于悲观的并发处理策略,总是认为只要不去做正确的同步措施,那就肯定会出问题,无论共享数据是否真的会出现竞争,它都要进行加锁、用户态核心太转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作
ü 非阻塞同步
基于冲突检测的乐观并发策略
先进行操作,如果没有其他线程争用共享数据,那就操作成功了;如果共享数据有争用,产生了冲突,那就再采取其他措施补救
需要操作和检测同步是原子操作
e) 锁优化
互斥同步对性能影响最大的是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作会给系统的并发性能带来很大的压力。为此引入偏向锁和轻量级锁。
ü 锁消除
指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测不可能存在共享数据竞争的锁进行消除
ü 锁粗化
原则上编写代码时建议将同步块的作用范围限制的尽量小,方便等待锁的线程能尽快拿到锁,但是如果一系列连续的操作对同一个对象反复的进行加锁、解锁操作,甚至加锁操作出现在循环体中,会造成严重的性能消耗
ü 锁升级
锁有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。
n 偏向锁
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护
n 轻量级锁
在无竞争的情况下,通过cas消除同步使用的互斥量
f) Concurrent包
ü 声明共享变量为volatile
ü 使用CAS的原子条件更新来实现线程的同步
ü 配合以volatile的读/写和CAS具有的volatile读和写的内存语义来实现线程间的通信