【笔记 Jvm-并发】
概述
并发处理 是使得Amadahl定律代替摩尔定律成为计算机性能发展源动力的根本原因;
Amdahl定律 通过系统中串行化与并行化的比重来描述多处理器系统所能获得到的运算加速能力;
摩尔定律 描述处理器晶体管数量与运行效率之间的发展关系;
硬件效率与一致性
计算机存储设备与处理器的运算速度存在几个数量级的差距,所以引入高速缓存Cache来作为内存与处理器之间的缓冲:即将运算所需用到的数据复制到Cache中,当运算结束后再同步回内存;
缓存一致性:在多处理器系统中,每个处理器都有自己的Cache,而它们又共享一个主内存,所以当多个处理器的运算任务都涉及到同一块内存区域时,将导致各自的缓存数据不一致;
内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象;
乱序执行优化:为了使得处理器内部的运算单元能够充分被利用,处理器会对输入代码进行乱序执行优化,并在运算之后将执行结果重组,能够保证计算结果正确但不保证输入代码各语句计算的先后顺序;
Java内存模型
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件与操作系统的内存访问差异,以实现让Java程序在各平台下都能达到一致的内存访问效果;
主内存与工作内存
Java内存模型的主要目标是定义程序各个变量的访问细节,即虚拟机中将变量存储到内存和从内存取出这样的底层细节;
Java内存模型规定所有的变量都存储在主内存(虚拟机内存的一部分),每条线程有自己的工作内存(保存了该线程使用到的变量主内存副本拷贝),线程对变量的读写操作只限制在工作内存;
不同线程也不能直接访问到对方工作内存中的变量,必须通过主内存进行传递;
内存间交互操作
Lock: 用于主内存的变量,将一个变量标识为一条线程独占的状态;
unlock: 作用于主内存的变量,释放后的变量可被其他线程锁定;
read: 作用于主内存的变量,将变量的值从主内存传输到线程工作内存;
load: 作用于工作内存的变量,它将read到的值放入工作内存的变量副本中;
use: 作用于工作内存变量,它把工作内存中的一个变量值传递到执行引擎,当虚拟机需要使用到变量的值的字节码指令时将会执行这个操作;
assign: 作用于工作内存变量,将从执行引擎收到的值赋值给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
store: 作用于工作内存变量,它把工作内存中一个变量的值传递到主内存中;
write: 作用于主内存变量,它将store的变量值放入到主内存变量中;
Java内存模型只要求read-load / store-write 操作必须按顺序执行,并没有保证是连续执行,即read-load/ store-write 之间可以插入其他指令;
volatile变量
可见性 保证此变量对所有线程的可见性,基于volatile的运算在并发下并非安全,因为Java内的操作并非原子操作;
禁止指令重排 设置了内存屏障,不能将后面的指令重排序到内存屏障之前的位置;
volitale变量的读操作性消耗与普通变量几乎没有区别,但是写操作会慢一些,因为它需要在代码中插入许多内存屏障指令保证处理器不发生乱序执行;
原子性
内存间交互操作八个动作均为原子性,其中虚拟机提供了monitorenter、monitorexit来隐式使用lock、unlock,反映到Java代码中就是同步块 synchronized关键字,即在synchronized中的操作也具备原子性;
可见性
可见性指的是当一个线程改变了共享变量的值,其他线程能够立即得知这个修改;
除了volatile,synchronized、final也能实现可见性;
有序性
Java语言提供了volatile和synchronized关键字来保证线程之间操作的有序性;
“天然”先行发生关系
指的是Java内存模型中定义的两项操作之间的偏序关系;
以下为Java内存模型中的“天然”先行发生关系,无需任何同步器协助,可在编码中直接使用;
程序次序规则:在一个线程内,按照程序流顺序,在前的操作先行于在后的操作;
管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;
volatile变量规则:对一个volatile变量的写操作先行于后面对该变量的读操作;
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
线程终止规则:线程中的所有操作都先行发生于此线程的终止检测,可通过Thread.join()/isAlive()检测线程已经终止执行;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可通过Thread.interrupted()方法检测到是否有中断发生;
对象终结规则:一个对象的初始化完成先行于它的finalize()方法的开始;
传递性:先行传递 A->B->C;
Java与线程
线程是CPU调度的基本单位;
Java Thread类与大部分的Java API有着显著的差别,它的所有关键方法都是声明为Native;
Native方法 意味着这个方法没有使用或无法使用平台无关的手段来实现;
线程实现主要有三种方式:
使用内核线程实现
内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负者将线程的任务映射到各个处理器上;
每个内核线程可以视为内核的一个分身,这样操作系统就有能力处理多件事情,支持多线程的内核就叫做多线程内核;
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口–轻量级进程,轻量级进程就是我们通常说的的线程,由于每个轻量级进程都有一个内核线程支持;
轻量级进程局限性
由于基于内核线程实现,所以各种线程操作,如创建、析构、同步,都需要进行系统调用,系统调用的代价性对较高,需要在用户态-内核态来回切换;
每个轻量级进程都需要一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源,即一个系统支持轻量级进程的数量是有限的;
使用用户线程实现
一个线程只要不是内核线程,就可以认为是用户线程;
用户线程的建立、同步、销毁、调度完全在用户态中完成,不需要内核的帮助;
如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量;
使用用户线程&轻量级进程混合实现
略;
Java线程调度
协同式调度 线程执行的时间由本身控制,在执行完之后再通知系统切换到另一个线程上,实现简单,没有线程同步问题;
抢占式调度 每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度;
线程优先级 当多个线程同时处于Ready状态时,优先级越高的线程就越容易被系统选择执行;
状态转换
在任意一个时间点,一个线程只能有其中的一种状态
新建:创建后尚未启动的线程处于这种状态;
运行:Runnable包括了操作系统线程状态中的Running/Ready,也就是处于此状态线的线程可能正在执行/有可能正在等待CPU分配执行时间;
无限期等待:处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒;
以下方法会让线程进入无限期等待状态:
未设置参数的Object.wait();
未设置参数的Thread.join();
LockSupport.park();
限期等待:处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其他线程显示唤醒,在一定时间之后他们会由系统自动唤醒;
以下方法会让线程进入限期等待状态:
Thread.sleep();
设置了Timeout参数的Object.wait();
设置了Timeout参数的Thread.join();
LockSupport.parkNanos();
LockSupport.parkUntil();
阻塞:阻塞状态 与 等待状态的区别是,阻塞状态在等待着获取到一个排他锁,这个事件将在另外一线程放弃这个锁的时候发生,而等待状态就是则是在等待一段时间或者唤醒动作的发生;
结束:已终止线程的线程状态,线程已经结束执行;
线程安全与锁优化
当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,则这个对象是线程安全的;
按照线程安全的“安全程度”由强至弱排序,可将Java语言中各种操作共享的数据分为以下五类:
不可变:
在Java语言中,不可变的对象一定是线程安全的,final关键字,只要一个不可变的对象正确地被构建出来。那其外部的可见状态就不会改变;
String类对象是一个典型的不可变对象,我们调用它的substring()/replace()/concat()这些方法都不会影响到它原来的值,只会返回一个新构造的字符串对象;
Long/Double/枚举类型、Number部分子类
AtomicInteger/AtomicLong则并非不可变 ?
绝对线程安全
Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全;
相对线程安全
Vector/HashTable/Collections/synchronizedCollection()等;
线程兼容:
线程兼容指的是对象本身并不是线程安全的但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用;
Java ApI中大部分的类都是属于线程兼容的,如之前定的Vector和HashTable相对应的集合类,ArrayList,HashMap类等;
线程对立
指的是调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码;
一个线程对立的例子是Thread类的suspend()和resume()方法;
线程安全的实现方法
互斥同步
同步指的是多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用;
互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式;
synchronized是java语言中重量级的操作
java.util.concurrent中的重入锁ReentrantLock,表现为原生语法上的互斥锁:
等待可中断 持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情;
公平锁 指的多个线程在等待同一个锁是,必须按照申请锁的时间顺序来获得锁,synchronized是公平锁,ReentrantLock默认情况是非公平的,但可以通过带布尔值的构造函数是用公平锁;
锁绑定多个条件 略;
非阻塞同步
互斥同步属于一种悲观并发策略,认为只要不去做正确的同步措施,就肯定会出现问题;
非阻塞同步 基于冲突检测的乐观并发策略,即先进性操作,如果没有其他线程争用则操作成功,如果产生冲突则再采取补偿措施,无需挂起线程;
锁优化
自旋锁
共享数据的锁定状态只会持续很短的一个时间,在这段时间去挂起和恢复锁并不值得,所以只需要让线程执行一个忙循环;
自适应自旋锁
着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间以及锁定拥有者状态来决定;
锁消除
锁消除指的是虚拟机在即时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除;
锁粗化
若虚拟机探测到有一连串的操作都对同一个对象加锁,则会将锁同步的范围扩大(粗化)到整个操作徐磊的外部;
轻量级锁
HotSpot虚拟机的对象头分为两部分信息
第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,这部分数据成为Mark Word,它是实现轻量级锁和偏向锁的关键;
另一部分用于存储指向方法区对象类型数据的指针;
在代码进入同步块的时候,如果此同步对象未被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储Mark Word拷贝;
然后虚拟机将使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,如果这个更新动作成功,则该线程拥有了该对象的锁,并且对象Mark Word的锁标志位将转变为“00”,即表示此对象处于轻量级锁状态;
偏向锁
目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能;
偏向锁会偏向于第一个获得它的进程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要进行同步;
。。。
就先到这了,建议还是去看原文《深入理解Java虚拟机》