Java内存模型(JMM)与线程

 1. JMM

JMM是JVM规范中定义的一种模型,来屏蔽掉各种硬件与操作系统的内存访问差异,实现Java程序可以在各种平台下都能达到一致的内存访问效果。

1. 1 volatile

volatile关键字是JVM中最轻量级的同步机制。

volatile作用:被volatile关键字修饰的变量具有两个特性:1)保证此变量对所有线程的可见性;2)禁止指令重排序优化。

可见性:指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。

指令重排序:保证在方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

关于JMM

JMM主要目标是定义程序中各个变量的访问规则,在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。

JMM规定所有的变量存储在主内存(Main Memory)中,每条线程还有自己独立的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(read, write)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程成间的工作内存都是独立的,是线程所私有的。即线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互如下图。

1.2 JMM几个特征 

JMM是围绕并发过程中如何处理原子性、可见性、和有序性三个特征所建立的。

原子性(Atomicity):JMM直接保证的原子性操作有lock、unlock、read、write、load、store、use、assign,即可以认为基本数据类型的访问读写是具备原子性的,如果需要更大范围的原子性保证,JMM通过其中的lock和unlock操作来满足需求,其中虚拟机未把lock和unlock操作直接开放给用户使用,但是提供了更高层次的字节码指令monitorenter和monitorexit进行隐式操作,这两个字节码指令反映到Java代码块就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性(Visibility):指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。在volatile中,JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递没接的方式实现可见性。volatile变量与普通变量的区别是,volatile保证了新值会立即同步到主内存和每次使用前立即从主内存刷新。因此可以说volatile在多线程操作时保证了变量的可见性,而普通变量则不能保证这一点。    synchronized和final关键字也实现了变量的可见性,同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获取的。final关键字的可见性指被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,其他线程中能看见final字段的值。

 有序性(Ordering):通过一些规则实现有序性(《深入理解Java虚拟机》),主内存与工作内存间的交互协议,原子性中的8个操作中某些组合操作必须符合一定顺序。有序性总结为:在本线程内观察,所有的操作都是有序的;在一个线程中观察另一个线程,所有的操作都是无序的。前半句指“线程内表现为穿行的语义”,后半句指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

1.3 先行发生原则

先行发生原则(happen-before):JMM中定义的两项操作间的偏序关系。如操作A先行发生于操作B,说的是发生操作B之前,操作A产生的影响能被B操作观察到,“影响”包括共享变量的值、发送消息、调用方法等。

该原则是判断数据是否存在竞争、行程是否安全的主要依据。

JMM下的一些“天然”先行发生关系:

程序次序规则(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 Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,中断检测有Thread.interrupted();

对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始;

传递性(Transitivity):A操作先行发生于B,B先行发生于C,则A先行发生于C。

以上先行发生原则无须任何同步器协助,如果两个操作之间的关系不在此列,并且无法从上面的规则中推到出来,它们就没有有序性保障,虚拟机可以对它们随意的进行重排序。

 2. Java与线程

2.1 实现

1. 使用内核线(Kernel-Level Thread, KLT)程实现,一般不会直接使用内核线程,通过使用内核线程的高级接口——轻量级进程(Light Weight Process, LWP),即通常意义上的线程。局限在需要进行系统调用,需要在用户态和内核态进行切换,代价相对较高;

2. 使用用户线程实现,优势在于不需要系统内核,劣势也在没有内核的支援,所有操作都要用户线程自己处理;

3. 使用用户线程加轻量级进程混合实现;

4. Java线程的实现,Sun JDK在Windows与Linux中使用一对一的线程模型实现,一条Java线程映射到一条轻量级进程中。

2.2 调度

线程调度室系统为线程分配处理器使用权的过程,分为协同式线程调度(Cooperative Thread-Scheduling)抢占式线程调度(Preemptive Threads-Scheduling)。

2.3 状态

线程具有5中状态,任意一个时间点,一个线程有且只有其中一个状态:

新建(New):创建后未启动;

运行(Running):包括了操作系统中的Running和Ready,即可能正在运行,也可能在等待资源(等待CPU分配执行时间);

无限期等待(Waiting):系统不会给线程分配CPU执行时间,要等待其他线程显式唤醒;

限期等待(Timed Waiting):也不会被分配CPU执行时间,但是不会无限期等待,一定时间后会被系统自动唤醒;

阻塞(Blocked):与等待的区别是,阻塞状态等待获取排它锁,进入同步区域的时候,线程进入该状态;

结束(Terminated):线程执行结束。

转换关系如下图:

 2.4 安全

安全的讨论限定于多个线程之间存在共享数据访问为前提。

将Java语言中各种操作共享数据分为5类:

1. 不可变(Immutable),不可变的对象一定是线程安全的,final关键字修饰;

2. 绝对线程安全

3. 相对线程安全,通常意义上所讲的线程安全,需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保护措施;

4. 线程兼容,指对象本身并不是线程安全的,但可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,即常说一个类不是线程安全的属于这种情况;

5. 线程对立,无论如何都不能在多线程环境中并发使用的代码。

通过3种方式实现线程安全

1. 互斥同步(Mutual Exclusion & Synchronization),常见的一种并发正确性保障手段。

同步指在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。

互斥是实现同步的一种手段,互斥的实现方式主要由临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)。

互斥是因,同步是果;互斥是方法,同步是目的。

通过线程阻塞和唤醒解决,但同时带来性能问题,因此也成为阻塞同步(Blocking Sychronizzation)。是一种悲观策略。

2. 非阻塞同步(Non-Blocking Sychronizzation),是一种乐观策略,一般是冲突的时候在进行加锁;

3. 无同步方案,同步只是保证共享数据争用时的正确性手段。

当不涉及共享数据的时候,就不需要同步操作,如一些代码天生就是线程安全的:1)可重入代码(Reentrant Code),特点有不依赖存储在堆上的数据和公用的系统资源、用到的状态量可有参数传入、不调用非可重入的方法。判定是当返回结果可预测的代码,只要输入一定,输出就不变。2)线程本地存储(Thread Local Storage),如果一段代码中所需要的数据必须与其他代码共享,就看共享数据的代码是否能保证在同一个线程中执行。

2.5 锁优化

1. 自旋锁与自适应自旋

2. 所消除,指VM即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

3. 锁粗化,一般情况是讲同步块的作用范围限制得尽量小,只在共享数据时进行同步,大部分时候如此,但是当一系列连续操作都是对同一个对象进行反复加锁和解锁的时候,可以把加锁同步的范围扩展(粗化)到更大一些的范围内,如此只需加一次锁。

4. 轻量级锁,相对于操作系统互斥量的实现而言的,因此传统的锁机制成为“重量级锁”,目的不是替换,而是减少重量级锁的的性能消耗。

其中对象头中的Mark Word(对象头中存储对象自身的运行时数据部分)是实现轻量级锁和偏向锁的关键。

轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量;

偏向锁是在无竞争的情况下把整个同步消除掉,连CAS操作都不做。

5. 偏向锁,目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

偏向锁、轻量级锁的状态转化即对象Mark Word的关系如下图:

具体的转化可以查询关于轻量级锁加锁的过程。

猜你喜欢

转载自www.cnblogs.com/baishouzu/p/12336826.html