Java内存模型与线程
1. 一致性问题
高速缓存可以解决处理器与内存的速度矛盾,却引入了新的问题:缓存一致性。
缓存一致协议:MSI MESI MOSI Synapse Firefly DragonProtocol
2. JMM Java Memory Model
被共享的变量:静态字段,构成数组对象的元素,实例字段,不包括局部变量和方法参数,其未被共享;
JMM规定所有变量存在主内存Main Memory 中,MM不是物理内存,是虚拟机内存的一部分;
另外,每个线程有自己的工作内存Working Memory, WM可类比高速缓存,保存了当前线程用到的变量的主存副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,不同线程也不能互相访问WM,线程间传递通过主内存进行;
主内存主要是Java堆中的对象,WM对应于虚拟机栈中的部分区域;
3. 内存间操作
JMM定义了八种操作:
lock 作用于内存中的变量;unlock 释放独占状态; read 从MM读到WM,为load做准备; load 将WM中的变量值放入变量副本;
use 作用于WM的变量,传递给执行引擎;assign 把从执行引擎获得的值赋值给WM的变量; store 将WM的副本的值传给MM,以备write;
write 把store中的值赋给变量
MM->WM read load; WM->MM store write
八种操作的规则:
read和load, store和write必须成对出现;不允许线程丢弃最近assign的值,WM中值变了,MM中必须变;
不允许线程无原因的同步到MM中(没有发生过任何assign);不允许线程在WM中,使用未被初始化的变量,变量必须被assign 或load过;
一个var可被lock多次,但要执行相同次数的unlock才被解锁;对一个var进行lock,会清空WM中的值,在搜索引擎重新使用这个变量时,需load或assign
没有lock,不能unlock;在unlock之前,需要先同步到MM中;
4. volitile
volatile变量只能保证可见性,一个线程修改,立刻修改到主线程,其他线程读取时直接读取;但却并不能保证并发状态下的的安全性。
在不符合以下两个情况的场景中,都需要加锁进行保护:
运算结果不依赖变量的当前值,或能够确保只有单一的线程修改变量的值;
变量不需要与其他的状态变量共同参与不变约束;
规则:read load use (load和use必须同时出现), assign store write;
5. 对long double的特殊处理(非原子协定)
原子性:JMM允许对64位的读写分为2个32位操作;但一般的虚拟机都当做了原子操作;
可见性:volitile 本身禁止指令重排序, synchronized 和 final (final时,只要不发生this对象逃逸就是原子的)
有序性:volitile和synchronized可以避免重排,synchronized性能受影响较多
6. Java与线程
6.1 每个java.lang.Thread就代表一个线程,Thread类和大部分的JavaAPI有显著的不同,所有的关键方法都声明为Native,意味着无法采用平台武钢的方法进行实现;
实现线程的方法有三种:
A 采用内核线程实现Kernel Thread KLT,每个KLT对应一个轻量级进程 Light Weight Process LWP, 程序一般不直接使用KLT而是采用LWP,内核线程方法的特点:一个LWP阻塞不影响其他;进程操作基于内核实现,会产生系统调用,开销较大,(KLT和LWP的个数比为1:1,成为一比一线程模型)
B 用户线程实现:LWP也属于用户线程,LWP是基于内核实现的,有系统开销;狭义上的用户实现完全建立在用户空间的线程上,系统内核不能感知到线程的存在,用户线程的建立、同步、销毁和调度完全在用户态中进行;优势和劣势都在于不需要系统内核支持,线程的创建、切换和调度都需要考虑,操作系统只负责把处理器资源分配到进程,其他的线程间如何避免阻塞等问题都比较困难;
C 混合实现:用户线程与LWP都存在,M:N的关系
D Java的线程实现:基于操作系统原生线程模型实现的;
6.2 Java的线程调度
协同式调度:线程执行时间由线程本身控制,线程把自己的事情做完,再通知系统切换到其他线程,简单,但有可能出现阻塞;
抢占式调度:由系统来分配执行时间,线程切换不由线程本身决定,Java可以设置线程的优先级;但优先级不靠谱,Windows有7种优先级,Java有10种,Solaris有2^31种;
6.3 线程的状态
New:创建未启动的线程;
Runable:包含OS中的Running和Ready
Waiting:不会分配时间片,等待被唤醒:调用了未设置时间信息的Object.wait() Thread.join()以及LockSupport.park()
Timed Waiting: Thread.sleep(), 设置参数的Object.wait() Thread.join(), LockSupport.parkNanos(), LockSUpport.parkUntil()方法
Blocked:阻塞,等待锁的释放
Terminated:已终止
7. 线程安全
代码本身封装了必备的正确性保障手段(如互斥同步等),令调用者无需关心多线程问题;
7.1 Java的线程安全分为五类:
A 不可变 Immutable: 基本类型加入final,或者非基本类型保证不可变(例如java.lang.String 的操作 substring()都不会改变string对象),保证对象的不可变,可以将对象中带有状态的变量都声明为final,于是变量就不再可变;(Long Double等)
B 绝对线程安全:要完全满足“不需要任何额外的同步措施”,Java API中很多标记为线程安全的,并不是绝对的线程安全,如vector;尽管vector的add() remove()等虽然已被声明为synchronized,在需要这些操作配合的情况下,仍然需要同步;
C 相对线程安全:保证单独操作是安全的,是一般意义上的线程安全,如Vector HashTable Colloections的synchronizedCollection()包装的集合
D 线程兼容:虽然对象本身不是线程安全的,但通过在调用端正确采用同步手段,可以在并发环境中安全使用,如ArrayList和HashMap等
E 线程对立:Thread的suspend和resume方法,如果两个线程都持有一个线程对象,一个尝试中断线程,一个尝试恢复;
7.2 线程安全的实现方法
7.2.1 互斥同步:采用临界区 Critical Section, 互斥量 Mutex和信号量 Semaphore
Java中最基本的是采用Synchronized关键字,在同步块前后加入monitorenter和monitorexit,进行加减计数器,计数器变为0,锁就被释放了;
synchronized是一个重量级方法,在同步块代码很小的情况下,尽量不使用;
还可使用java.util.concurrent中的ReentrantLock来实现同步,有三个功能:等待可中断(正在等待的线程可以先放弃等待);可实现公平锁(先申请先获得);锁绑定多个条件(synchronized中,锁对象的wait notify notifyAll可实现一个隐含的条件,如果要和多个条件关联,需要添加新锁,但reentrantlock不需要,多次盗用newCondition即可)
优先采用synchronzied, 线程数目小的时候,syn性能要好于lock,syn有较大的优化空间;
7.2.2 非阻塞同步
互斥同步是阻塞同步,非阻塞同步是指先进行操作,然后检测是否有共享数据冲突,如果有则继续尝试,直到操作成功;
7.2.3 无同步方案:不需要同步就是安全的
可重入代码:随时中断,控制权恢复后,程序没有错误;不依赖于堆上的数据和系统资源;
线程本地存储:将独享变量实现本地存储? java.lang.ThreadLocalMap
7.3 锁优化
自旋锁:在多个处理器的情况下,如果一个线程需要等待,先不让其等待,先进行一会儿自旋,看占锁的线程会不会马上释放锁;
锁消除:采用逃逸分析技术,如果堆上的数据不会逃逸出去被其他线程访问,则当成栈上操作,认为是线程私有的;
锁粗化:对很多短代码的锁的组合,会扩展锁,减少加锁和释放锁的次数;
轻量级锁:对象有一个Mark Word存放hashcode, GC 分代年龄,和锁标志位,锁01为没有锁,在进入同步块时,先转为00轻量级锁,如果成功,则执行同步块,否则变为10,升级为重量级锁,后边等待锁的线程都要等待;使用CAS在统计角度上提高性能,大部分情况下无竞争
偏向锁:01偏向模式,偏向模式下,不需要CAS,直接执行,当有线程请求锁时,则退化为轻量级锁