关于指令重排序的概念,比较复杂,很差理解。咱们从一个例子分析:html
public class SimpleHappenBefore { /** 这是一个验证结果的变量 */ private static int a=0; /** 这是一个标志位 */ private static boolean flag=false; public static void main(String[] args) throws InterruptedException { //因为多线程状况下未必会试出重排序的结论,因此多试一些次 for(int i = 0; i < 1000; i++){ ThreadA threadA=new ThreadA(); ThreadB threadB=new ThreadB(); threadA.start(); threadB.start(); //这里等待线程结束后,重置共享变量,以使验证结果的工做变得简单些. threadA.join(); threadB.join(); a = 0; flag = false; } } static class ThreadA extends Thread{ public void run(){ a = 1; flag = true; } } static class ThreadB extends Thread{ public void run(){ if(flag){ a = a * 1; } if(a == 0){ System.out.println("ha,a==0"); } } } }
一个简单的展现Happen-Before的例子.
这里有两个共享变量:a和flag,初始值分别为0和false.在ThreadA中先给a=1,而后flag=true.java
若是按照有序的话,那么在ThreadB中若是if(flag)成功的话,则应该a=1,而a=a*1以后a仍然为1,下方的if(a==0)应该永远不会为真,永远不会打印.算法
但实际状况是:在试验100次的状况下会出现0次或几回的打印结果,而试验1000次结果更明显,有十几回打印.编程
在虚拟机层面,为了尽量减小内存操做速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照本身的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽量充分地利用CPU。缓存
拿上面的例子来讲:假如不是a=1的操做,而是a=new byte[1024*1024](分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,仍是先执行下面那句flag=true呢?显然,先执行flag=true能够提早使用CPU,加快总体效率,固然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种状况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。无论谁先开始,总以后面的代码在一些状况下存在先结束的可能。安全
无论怎么重排序,单线程程序的执行结果不能被改变。编译器、运行时和处理器都必须遵照“as-if-serial”语义。拿个简单例子来讲,多线程
public void execute(){ int a = 0; int b = 1; int c = a+b; }
这里a=0,b=1两句能够随便排序,不影响程序逻辑结果,但c=a+b这句必须在前两句的后面执行。并发
在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必须存在happens-before关系。app
happens-before原则很是重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,咱们解决在并发环境下两操做之间是否可能存在冲突的全部问题。
happens-before原则定义以下:框架
- 若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。
重排序在多线程环境下出现的几率仍是挺高的,在关键字上有volatile和synchronized能够禁用重排序,除此以外还有一些规则,也正是这些规则,使得咱们在平时的编程工做中没有感觉到重排序的坏处。
若是不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序,也就是“若是在本线程中观察,全部的操做都是有序的;若是在一个线程中观察另一个线程,则不符合以上规则的都是无序的”,所以,若是咱们的多线程程序依赖于代码书写顺序,那么就要考虑是否符合以上规则,若是不符合就要经过一些机制使其符合,最经常使用的就是synchronized、Lock以及volatile修饰符。
上面八条是原生Java知足Happens-before关系的规则,可是咱们能够对他们进行推导出其余知足happens-before的规则:
happen-before原则是JMM中很是重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。
volatile
至关于synchronized
的弱实现,相似于synchronized
的语义,可是没有锁机制。在JDK及开源框架随处可见,可是在JDK6以后synchronized
关键字性能被大幅优化以后,几乎没有使用了场景。
第一条语义:JMM不会对volatile
指令的操做进行重排序。这个保证了对volatile变量的操做时按照指令的出现顺序执行的。
第二条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其余线程能看到变量X的变更,更详细地说是要符合如下两个规则:
虽然volatile
字段保证了可见性,可是因为缺乏同步机制,因此volatile的字段的操做不是原子性的,并不能保证线程安全。
一般应用场景以下:
volatile boolean done = flase; //... while(!done){ // ... }
独占锁是一种悲观锁,synchronized就是一种独占锁,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。
CAS 操做包含三个操做数——内存位置(V)、预期原值(A)和新值(B)。若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”
一般将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来得到新值 B,而后使用 CAS 将 V 的值从 A 改成 B。若是 V 处的值还没有同时更改,则 CAS 操做成功。
CAS其底层是经过CPU的1条指令来完成3个步骤,所以其自己是一个原子性操做,不存在其执行某一个步骤的时候而被中断的可能。
从性能角度考虑:
若是使用锁来进行并发控制,当某一个线程(T1)抢占到锁以后,那么其余线程再尝试去抢占锁时就会被挂起,当T1释放锁以后,下一个线程(T2)再抢占到锁后而且从新恢复到原来的状态大约须要通过8W个时钟周期。
假设咱们业务代码自己并不具有很复杂的操做,执行整个操做可能就花费3-10个时钟周期左右,那么当咱们使用无锁操做时,线程T1和线程T2对共享变量进行并发的CAS操做,假设T1成功了,T2最多再执行一次,它执行屡次的所消耗的时间远远小于因为线程所挂起到恢复所消耗的时间,所以无锁的CAS操做在性能上要比同步锁高不少。
示例代码:
public class SimulatedCAS { private int value; public synchronized int getValue() { return value; } public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (value == expectedValue) value = newValue; return oldValue; } }
非阻塞算法:一个线程的失败或者挂起不该该影响其余线程的失败或挂起的算法。
基于CAS的并发算法称为非阻塞算法,CAS 操做成功仍是失败,在任何一种状况中,它都在可预知的时间内完成。若是 CAS 失败,调用者能够重试 CAS 操做或采起其余适合的操做。下面显示了从新编写的计数器类来使用 CAS 替代锁定:
public class CasCounter { private SimulatedCAS value; public int getValue() { return value.getValue(); } public int increment() { int oldValue = value.getValue(); while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue) oldValue = value.getValue(); return oldValue + 1; } }
不管是直接的仍是间接的,几乎 java.util.concurrent 包中的全部类都使用原子变量,而不使用同步。相似 ConcurrentLinkedQueue 的类也使用原子变量直接实现无等待算法,而相似 ConcurrentHashMap 的类使用 ReentrantLock 在须要时进行锁定。而后, ReentrantLock 使用原子变量来维护等待锁定的线程队列。
CAS策略有以下须要注意的事项: