java内存模型
Java内存模型用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果
java内存模型的主要目的是定义程序中各种变量的访问规则。
java内存模型规定了:
- 所有变量(实例字段、静态字段等非线程私有变量)都存储在主内存中。
- 每个线程有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(如读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量。
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

volatile关键字
volatile关键字有两个语义:
- 保证可见性
- 静止指令重排序
保证可见性实现原理
- 修改volatile变量时会强制将修改后的值刷新的主内存中。
- 每次读取volatile变量时都从主内存中重新读。
禁止指令重排序优化
1 | //单例DCL(双锁检测) |
指令重排分析
instance = new Singleton()
该方法其实有3步:
- 分配内存空间何内存地址
- 初始化对象
- 将实例指向分配的内存地址
第二步和第三步没有数据依赖关系,单线程下指令重排不影响执行结果,因此编译器和cpu允许重排优化的行为。即可能出现第三步先于第二步执行,但对象的初始化还没有完成,造成线程安全问题。(另一个线程在第二步的时候获取到尚未初始化的单例)
long和double的非原子协定
long和dubble的非原子协定:虚拟机允许没有被volatile修饰的64位数据的读写操作分为两次32位的操作来进行。
读取到半个变量这种情况非常罕见,商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待。
32位虚拟机下,对long变量的访问存在非原子性访问的风险。,可以使用volatile关键字保证其原子性。
原子性、可见性与有序性
volatile保证了可见性和有序性。
synchronized三个特性都能保证。
happen-before原则
先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对于同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则(Volatile Variable Rule):对于一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间的先后顺序。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interrupt Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
线程底层实现
线程实现的方式主要有三种:
- 内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级进程混合实现
内核线程实现
内核线程(KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口–轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都由一个内核线程支持。这种轻量级进程与内核进程之间的1:1关系称为一对一的线程模型。

局限性:
- 各种线程操作需要进行系统调用,系统调用代价相对较高,需要在用户态和内核态中来回切换。
- 每个轻量级进程都需要由一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源,一个系统支持轻量级进程的数量是有限的。
使用用户线程实现
广义:一个线程只要不是内核线程,就可以认为是用户线程。
狭义:用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在。用户线程的建立、同步、销毁和调度完全在用户状态中完成,不要内核帮助。
优点:这种线程不需要切换到内核态,操作非常快速且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是有用户线程实现的。
缺点:没有内核支持,线程之间调度实现起来非常困难。
这种进程与用户线程之间1:N的关系称为一对多线程模型。

使用用户线程加轻量级进程混合实现
操作系统提供支持的轻量级进程作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能以及处理器映射。
用户线程和轻量级进程的数量比是不定的,即为N:M的关系,称为多对多线程模型

java线程的实现
JDK1.2之前,基于称为绿色线程的用户线程实现的,1.2中线程模型替换为基于操作系统原生线程模型实现。
线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要方式有两种:
- 协同式线程调度:线程把自己工作执行完以后,主动通知系统切换到另一个线程上。(缺点:线程执行时间不受控制、死循环)
- 抢占式线程调度:每个线程由系统来分配执行时间。
线程优先级
Java有10个优先级,两个线程同时处于ready状态时,优先级越高越容易被系统选择执行(不同操作系统的线程优先级定义不一样,和java线程优先级的映射关系不同,建议只用默认三个 MIN,NORM,MAX。)
协程
//todo
线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象时线程安全的。
我们可以将java语言中各种操作的共享数据分为以下五类:
- 不可变:final
- 绝对线程安全:满足上面定义,Vector也做不到绝对线程安全(两个线程去,一个去get 一个去remove)
- 相对线程安全:通常意义上的线程安全,保证对这个对象单独的操作是线程安全的。对于特定顺序的连续调用需要我们额外同步。(例如Vector HashTable)
- 线程兼容:对象本身不是线程安全的,可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用。
- 线程对立:无论调用段是否采取了同步措施,都无法在多线程环境中并发使用的代码。(很少出现)
线程安全的实现方法
- 互斥同步:互斥同步最主要的问题是进行线程阻塞和换下所带来的性能问题,因此也被称为阻塞同步,悲观锁。
- synchronized(非公平 可重入)
- ReentrantLock
synchronized与ReentrantLock主要区别是:
ReentrantLock更加灵活,等待可中断,可实现公平锁,以及锁可以绑定多个条件
互斥同步的缺点是:如果升级到重量级锁,需要操作系统进行系统调用,系统调用则需要在用户态和内核态之间切换,代价较高,因此才会有乐观锁。
- 非阻塞同步(乐观并发策略):如果没有其他线程争用共享数据,直接操作成功;如果有其他线程争用共享数据,产生了冲突,就会采取其他补偿措施(常见的补偿措施就是不断重试,直到成功为止)。不会将线程挂起。避免在用户态和内核态之间频繁切换。(CAS就是乐观并发策略)
- 无同步方案:
- 可重入代码:不依赖全局变量、公用资源的代码。
- 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,可以看下共享数据的代码是否能保证在一个线程里面执行。
CAS
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。解决方案:递增版本号。
循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。