众所周知,Java是多线程的。可是,Java对多线程的支持实际上是一把双刃剑。一旦涉及到多个线程操做共享资源的状况时,处理很差就可能产生线程安全问题。线程安全性多是很是复杂的,在没有充足的同步的状况下,多个线程中的操做执行顺序是不可预测的。html
Java里面进行多线程通讯的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操做的原子性,咱们能够认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。java
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。算法
悲观锁:老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。再好比Java里面的同步原语synchronized关键字的实现也是悲观锁。数据库
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。编程
Java在JDK1.5以前都是靠synchronized关键字保证同步的,这种经过使用一致的锁定协议来协调对共享状态的访问,能够确保不管哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,因此能够说synchronized是悲观锁。缓存
悲观锁机制存在如下问题:安全
一、在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。数据结构
二、一个线程持有锁会致使其它全部须要此锁的线程挂起。多线程
三、若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。并发
而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。
与锁相比,volatile变量是一个更轻量级的同步机制,由于在使用这些变量时不会发生上下文切换和线程调度等操做,可是volatile不能解决原子性问题,所以当一个变量依赖旧值时就不能使用volatile变量。所以对于同步最终仍是要回到锁机制上来。
乐观锁( Optimistic Locking)在上文已经说过了,其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据通常状况下不会产生并发冲突,因此在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,若是发现并发冲突了,则让返回用户错误的信息,让用户决定如何去作。
上面提到的乐观锁的概念中其实已经阐述了它的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。
CAS(Compare And Swap),即比较并交换。是解决多线程并行状况下使用锁形成性能损耗的一种机制,CAS操做包含三个操做数——内存位置(V)、预期原值(A)和新值(B)。若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。不管哪一种状况,它都会在CAS指令以前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;若是包含该值,则将B放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可”。这其实和乐观锁的冲突检查+数据更新的原理是同样的。
在JDK1.5 中新增 java.util.concurrent (J.U.C)就是创建在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。因此J.U.C在性能上有了很大的提高。
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不该该影响其余线程的失败或挂起的算法。
以 java.util.concurrent 中的 AtomicInteger 为例,看一下在不使用锁的状况下是如何保证线程安全的。主要理解 getAndIncrement 方法,该方法的做用至关于 ++i 操做。
public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } }
在没有锁的机制下,字段value要借助volatile原语,保证线程间的数据是可见性。这样在获取变量的值的时候才能直接读取。而后来看看 ++i 是怎么作到的。
getAndIncrement 采用了CAS操做,每次从内存中读取数据而后将此数据和 +1 后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。
而 compareAndSet 利用JNI(Java Native Interface)来完成CPU指令的操做:
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
首先能够看到AtomicInteger类在域中声明了这两个私有变量unsafe和valueOffset。其中unsafe实例采用Unsafe类中静态方法getUnsafe()获得,可是这个方法若是咱们写的时候调用会报错,由于这个方法在调用时会判断类加载器,咱们的代码是没有“受信任”的,而在jdk源码中调用是没有任何问题的;valueOffset这个是指类中相应字段在该类的偏移量,在这里具体便是指value这个字段在AtomicInteger类的内存中相对于该类首地址的偏移量。
而后能够看一个有一个静态初始化块,这个块的做用便是求出value这个字段的偏移量。具体的方法使用的反射的机制获得value的Field对象,再根据objectFieldOffset这个方法求出value这个变量内存中在该对象中的偏移量。
其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);相似以下逻辑:
if (this == expect) { this = update return true; } else { return false; }
那么比较this == expect,替换this = update,compareAndSwapInt实现这两个步骤的原子性呢? 参考CAS的原理
CAS原理:
CAS经过调用JNI的代码实现的。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
下面从分析比较经常使用的CPU(intel x86)来解释CAS的实现原理。
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
能够看到这是个本地方法调用。这个本地方法在JDK中依次调用的C++代码为:
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。若是程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,若是程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不须要lock前缀提供的内存屏障效果)。
好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,但可能存在潜藏的问题。
好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,但可能存在潜藏的问题。以下所示:
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,而后但愿用CAS将栈顶替换为B:
head.compareAndSet(A,B);
在T1执行上面这条指令以前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构以下图,而对象B此时处于游离状态:
此时轮到线程T1执行CAS操做,检测发现栈顶仍为A,因此CAS成功,栈顶变为B,但实际上B.next为null,因此此时的状况变为:
其中堆栈中只有B一个元素,C和D组成的链表再也不存在于堆栈中,无缘无故就把C、D丢掉了。
以上就是因为ABA问题带来的隐患,各类乐观锁的实现中一般都会用版本戳version来对记录或对象标记,避免并发操做带来的问题。
所以AtomicStampedReference/AtomicMarkableReference就颇有用了。能够用来避免ABA问题。
AtomicMarkableReference
类描述的一个<Object,Boolean>的对,能够原子的修改Object或者Boolean的值,这种数据结构在一些缓存或者状态描述中比较有用。这种结构在单个或者同时修改Object/Boolean的时候可以有效的提升吞吐量。AtomicStampedReference
类维护带有整数“标志”的对象引用,能够用原子方式对其进行更新。对比AtomicMarkableReference 类的<Object,Boolean>,AtomicStampedReference维护的是一种相似<Object,int>的数据结构,其实就是对对象(引用)的一个并发计数(标记版本戳stamp)。可是与AtomicInteger 不一样的是,此数据结构能够携带一个对象引用(Object),而且可以对此对象和计数同时进行原子操做。
以AtomicStampedReference为例。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference,它经过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。这个类的compareAndSet方法是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean compareAndSet( V expectedReference,//预期引用 V newReference,//更新后的引用 int expectedStamp, //预期标志 int newStamp //更新后的标志 )
例以下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操做,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:
package concur.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class ABA { private static AtomicInteger atomicInt = new AtomicInteger(100); private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0); public static void main(String[] args) throws InterruptedException { Thread intT1 = new Thread(new Runnable() { @Override public void run() { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); } }); Thread intT2 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean c3 = atomicInt.compareAndSet(100, 101); System.out.println(c3); //true } }); intT1.start(); intT2.start(); intT1.join(); intT2.join(); Thread refT1 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); } }); Thread refT2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedRef.getStamp(); System.out.println("before sleep : stamp = " + stamp); // stamp = 0 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1 boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1); System.out.println(c3); //false } }); refT1.start(); refT2.start(); } }
自旋CAS(不成功,就一直循环执行,直到成功)若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。
当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。
一、对于资源竞争较少(线程冲突较轻)的状况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操做额外浪费消耗cpu资源;而CAS基于硬件实现,不须要进入内核,不须要切换线程,操做自旋概率较少,所以能够得到更高的性能。
二、对于资源竞争严重(线程冲突严重)的状况,CAS自旋的几率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充:synchronized在jdk1.6以后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但得到了高吞吐量。在线程冲突较少的状况下,能够得到和CAS相似的性能;而线程冲突严重的状况下,性能远高于CAS。
因为java的CAS同时具备 volatile 读和volatile写的内存语义,所以Java线程之间的通讯如今有了下面四种方式:
- A线程写volatile变量,随后B线程读这个volatile变量。
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操做,这是在多处理器中实现同步的关键(从本质上来讲,可以支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,所以任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操做的原子指令)。同时,volatile变量的读/写和CAS能够实现线程之间的通讯。把这些特性整合在一块儿,就造成了整个concurrent包得以实现的基石。若是咱们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 而后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具备的volatile读和写的内存语义来实现线程之间的通讯。
AQS、非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),concurrent包中的基础类都是使用这种模式来实现的。而concurrent包中的高层类又是依赖于这些基础类来实现的。从总体来看,concurrent包的实现示意图以下:
Java调用new object()会建立一个对象,这个对象会被分配到JVM的堆中。那么这个对象究竟是怎么在堆中保存的呢?
首先,new object()执行的时候,这个对象须要多大的空间,实际上是已经肯定的,由于java中的各类数据类型,占用多大的空间都是固定的(对其原理不清楚的请自行Google)。那么接下来的工做就是在堆中找出那么一块空间用于存放这个对象。
在单线程的状况下,通常有两种分配策略:
指针碰撞:这种通常适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工做只是将指针像空闲内存一侧移动对象大小的距离便可。
空闲列表:这种适用于内存非规整的状况,这种状况下JVM会维护一个内存列表,记录哪些内存区域是空闲的,大小是多少。给对象分配空间的时候去空闲列表里查询到合适的区域而后进行分配便可。
可是JVM不可能一直在单线程状态下运行,那样效率太差了。因为再给一个对象分配内存的时候不是原子性的操做,至少须要如下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:
CAS:实际上虚拟机采用CAS配合上失败重试的方式保证更新操做的原子性,原理和上面讲的同样。
TLAB:若是使用CAS其实对性能仍是会有影响的,因此JVM又提出了一种更高级的优化策略:每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部须要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光须要从新分配内存的时候才会进行CAS操做分配更大的内存空间。
虚拟机是否使用TLAB,能够经过-XX:+/-UseTLAB参数来进行配置(jdk5及之后的版本默认是启用TLAB的)。
参考资料:
乐观锁的一种实现方式——CAS
Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
CAS原理 Java SE1.6中的Synchronized
JAVA并发编程: CAS和AQS
Java CAS 和ABA问题
CAS原理分析
慕课网高并发实战(四)- 线程安全性
java Unsafe类中compareAndSwap相关介绍