本文主要是介绍ava并发编程-无锁CAS与Unsafe类及其并发包Atomic,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
在前面一篇博文中,我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其原理是通过当前线程持有当前对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,也就保证了线程安全。但在本篇中,我们将会详聊另外一种反向而行的并发策略,即无锁并发,即不加锁也能保证并发执行的安全性。
本篇的思路是先阐明无锁执行者CAS的核心算法原理然后分析Java执行CAS的实践者Unsafe类,该类中的方法都是native修饰的,因此我们会以说明方法作用为主介绍Unsafe类,最后再介绍并发包中的Atomic系统使用CAS原理实现的并发类,以下是主要内容
- 无锁的概念
- 无锁的执行者-CAS
- CAS
- CPU指令对CAS的支持
- 鲜为人知的指针 Unsafe类
- 并发包中的原子操作类Atomic系列
- 原子更新基本类型
- 原子更新引用
- 原子更新数组
- 原子更新属性
- CAS的ABA问题及其解决方案
- 再谈自旋锁
无锁的概念
在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。
无锁的执行者-CAS
CAS
CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下
执行函数:CAS(V,E,N)
其包含3个参数
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下
由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。
CPU指令对CAS的支持
或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
鲜为人知的指针: Unsafe类
Unsafe类存在于sun.misc
包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,据说Oracle正在计划从Java 9中去掉Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:
内存管理,Unsafe类中存在直接操作内存的方法
//分配内存指定大小的内存 public native long allocateMemory(long bytes); //根据给定的内存地址address设置重新分配指定大小的内存 public native long reallocateMemory(long address, long bytes); //用于释放allocateMemory和reallocateMemory申请的内存 public native void freeMemory(long address); //将指定对象的给定offset偏移量内存块中的所有字节设置为固定值 public native void setMemory(Object o, long offset, long bytes, byte value); //设置给定内存地址的值 public native void putAddress(long address, long x); //获取指定内存地址的值 public native long getAddress(long address);//设置给定内存地址的long值 public native void putLong(long address, long x); //获取指定内存地址的long值 public native long getLong(long address); //设置或获取指定内存的byte值 public native byte getByte(long address); public native void putByte(long address, byte x); //其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同//操作系统的内存页大小 public native int pageSize();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
提供实例对象新途径。
//传入一个对象的class并创建该实例对象,但不会调用构造方法 public native Object allocateInstance(Class cls) throws InstantiationException;
- 1
- 2
类和实例对象以及变量的操作,主要方法如下
//获取字段f在实例对象中的偏移量 public native long objectFieldOffset(Field f); //静态属性的偏移量,用于在对应的Class对象中读写静态属性 public native long staticFieldOffset(Field f); //返回值就是f.getDeclaringClass() public native Object staticFieldBase(Field f);//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址, //通过偏移量便可得到该对象的变量,进行各种操作 public native int getInt(Object o, long offset); //设置给定对象上偏移量的int值 public native void putInt(Object o, long offset, int x);//获得给定对象偏移量上的引用类型的值 public native Object getObject(Object o, long offset); //设置给定对象偏移量上的引用类型的值 public native void putObject(Object o, long offset, Object x); //其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见 public native void putIntVolatile(Object o, long offset, int x); //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。 public native int getIntVolatile(Object o, long offset);//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile及getIntVolatile相同,引用类型putObjectVolatile也一样。//与putIntVolatile一样,但要求被操作字段必须有volatile修饰 public native void putOrderedInt(Object o,long offset,int x);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
下面通过一个简单的Demo来演示上述的一些方法以便加深对Unsafe类的理解
public class UnSafeDemo {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {// 通过反射得到theUnsafe对应的Field对象Field field = Unsafe.class.getDeclaredField("theUnsafe");// 设置该Field为可访问field.setAccessible(true);// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的Unsafe unsafe = (Unsafe) field.get(null);System.out.println(unsafe);//通过allocateInstance直接创建对象User user = (User) unsafe.allocateInstance(User.class);Class userClass = user.getClass();Field name = userClass.getDeclaredField("name");Field age = userClass.getDeclaredField("age");Field id = userClass.getDeclaredField("id");//获取实例变量name和age在对象内存中的偏移量并设置值unsafe.putInt(user,unsafe.objectFieldOffset(age),18);unsafe.putObject(user,unsafe.objectFieldOffset(name),"android TV");// 这里返回 User.class,Object staticBase = unsafe.staticFieldBase(id);System.out.println("staticBase:"+staticBase);//获取静态变量id的偏移量staticOffsetlong staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("id"));//获取静态变量的值System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));//设置值unsafe.putObject(staticBase,staticOffset,"SSSSSSSS");//获取静态变量的值System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));//输出USERSystem.out.println("输出USER:"+user.toString());long data = 1000;byte size = 1;//单位字节//调用allocateMemory分配内存,并获取内存地址memoryAddresslong memoryAddress = unsafe.allocateMemory(size);//直接往内存写入数据unsafe.putAddress(memoryAddress, data);//获取指定内存地址的数据long addrData=unsafe.getAddress(memoryAddress);System.out.println("addrData:"+addrData);/*** 输出结果:sun.misc.Unsafe@6f94fa3estaticBase:class geym.conc.ch4.atomic.User设置前的ID:USER_ID设置前的ID:SSSSSSSS输出USER:User{name='android TV', age=18', id=SSSSSSSS'}addrData:1000*/} }class User{public User(){System.out.println("user 构造方法被调用");}private String name;private int age;private static String id="USER_ID";@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'\'' +", id=" + id +'\'' +'}';} }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
虽然在Unsafe类中存在getUnsafe()方法,但该方法只提供给高级的Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在Demo中使用了反射技术获取了Unsafe实例对象并进行相关操作。
public static Unsafe getUnsafe() {Class cc = sun.reflect.Reflection.getCallerClass(2);if (cc.getClassLoader() != null)throw new SecurityException("Unsafe");return theUnsafe;}
- 1
- 2
- 3
- 4
- 5
- 6
数组操作
//获取数组第一个元素的偏移地址 public native int arrayBaseOffset(Class arrayClass); //数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置 public native int arrayIndexScale(Class arrayClass);
- 1
- 2
- 3
- 4
- 5
CAS 操作相关
CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值, //expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。 public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这里还需介绍Unsafe类中JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法,如下
//1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值public final int getAndAddInt(Object o, long offset, int delta) {int v;do {//获取内存中最新值v = getIntVolatile(o, offset);//通过CAS操作} while (!compareAndSwapInt(o, offset, v, v + delta));return v;}//1.8新增,方法作用同上,只不过这里操作的long类型数据public final long getAndAddLong(Object o, long offset, long delta) {long v;do {v = getLongVolatile(o, offset);} while (!compareAndSwapLong(o, offset, v, v + delta));return v;}//1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值public final int getAndSetInt(Object o, long offset, int newValue) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, newValue));return v;}// 1.8新增,同上,操作的是long类型public final long getAndSetLong(Object o, long offset, long newValue) {long v;do {v = getLongVolatile(o, offset);} while (!compareAndSwapLong(o, offset, v, newValue));return v;}//1.8新增,同上,操作的是引用类型数据public final Object getAndSetObject(Object o, long offset, Object newValue) {Object v;do {v = getObjectVolatile(o, offset);} while (!compareAndSwapObject(o, offset, v, newValue));return v;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
上述的方法我们在稍后的Atomic系列分析中还会见到它们的身影。
挂起与恢复
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。 public native void park(boolean isAbsolute, long time); //终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法, public native void unpark(Object thread);
- 1
- 2
- 3
- 4
- 5
内存屏障
这里主要包括了loadFence、storeFence、fullFence等方法,这些方法是在Java 8新引入的,用于定义内存屏障,避免代码重排序,与Java内存模型相关,感兴趣的可以看博主的另一篇博文全面理解Java内存模型(JMM)及volatile关键字,这里就不展开了
//在该方法之前的所有读操作,一定在load屏障之前执行完成 public native void loadFence(); //在该方法之前的所有写操作,一定在store屏障之前执行完成 public native void storeFence(); //在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能 public native void fullFence();
- 1
- 2
- 3
- 4
- 5
- 6
其他操作
//获取持有锁,已不建议使用 @Deprecated public native void monitorEnter(Object var1); //释放锁,已不建议使用 @Deprecated public native void monitorExit(Object var1); //尝试获取锁,已不建议使用 @Deprecated public native boolean tryMonitorEnter(Object var1);//获取本机内存的页数,这个值永远都是2的幂次方 public native int pageSize(); //告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类 public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); //加载一个匿名类 public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); //判断是否需要加载一个类 public native boolean shouldBeInitialized(Class<?> c); //确保类一定被加载 public native void ensureClassInitialized(Class<?> c)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
并发包中的原子操作类(Atomic系列)
通过前面的分析我们已基本理解了无锁CAS的原理并对Java中的指针类Unsafe类有了比较全面的认识,下面进一步分析CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic
包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型。
原子更新基本类型
原子更新基本类型主要包括3个类:
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,鉴于AtomicInteger的源码不多,我们直接看源码
public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// 获取指针类Unsafeprivate static final Unsafe unsafe = Unsafe.getUnsafe();//下述变量value在AtomicInteger实例对象内的内存偏移量private static final long valueOffset;static {try {//通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移//通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}//当前AtomicInteger封装的int变量valueprivate volatile int value;public AtomicInteger(int initialValue) {value = initialValue;}public AtomicInteger() {}//获取当前最新值,public final int get() {return value;}//设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。public final void set(int newValue) {value = newValue;}//最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载public final void lazySet(int newValue) {unsafe.putOrderedInt(this, valueOffset, newValue);}//设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法public final int getAndSet(int newValue) {return unsafe.getAndSetInt(this, valueOffset, newValue);}//如果当前值为expect,则设置为update(当前值指的是value变量)public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}//当前值加1返回旧值,底层CAS操作public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);}//当前值减1,返回旧值,底层CAS操作public final int getAndDecrement() {return unsafe.getAndAddInt(this, valueOffset, -1);}//当前值增加delta,返回旧值,底层CAS操作public final int getAndAdd(int delta) {return unsafe.getAndAddInt(this, valueOffset, delta);}//当前值加1,返回新值,底层CAS操作public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}//当前值减1,返回新值,底层CAS操作public final int decrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, -1) - 1;}//当前值增加delta,返回新值,底层CAS操作public final int addAndGet(int delta) {return unsafe.getAndAddInt(this, valueOffset, delta) + delta;}//省略一些不常用的方法....
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger是基于无锁实现的,这里重点分析自增操作实现过程,其他方法自增实现原理一样。
//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
- 1
- 2
- 3
- 4
我们发现AtomicInteger类中所有自增或自减的方法都间接调用Unsafe类中的getAndAddInt()方法实现了CAS操作,从而保证了线程安全,关于getAndAddInt其实前面已分析过,它是Unsafe类中1.8新增的方法,源码如下
//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));return v;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可看出getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt方法,是一个CAS操作方法。这里需要注意的是,上述源码分析是基于JDK1.8的,如果是1.8之前的方法,AtomicInteger源码实现有所不同,是基于for死循环的,如下
//JDK 1.7的源码,由for的死循环实现,并且直接在AtomicInteger实现该方法,
//JDK1.8后,该方法实现已移动到Unsafe类中,直接调用getAndAddInt方法即可
public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
ok~,下面简单看个Demo,感受一下AtomicInteger使用方式
public class AtomicIntegerDemo {//创建AtomicInteger,用于自增操作static AtomicInteger i=new AtomicInteger();public static class AddThread implements Runnable{public void run(){for(int k=0;k<10000;k++)i.incrementAndGet();}}public static void main(String[] args) throws InterruptedException {Thread[] ts=new Thread[10];//开启10条线程同时执行i的自增操作for(int k=0;k<10;k++){ts[k]=new Thread(new AddThread());}//启动线程for(int k=0;k<10;k++){ts[k].start();}for(int k=0;k<10;k++){ts[k].join();}System.out.println(i);//输出结果:100000}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
在Demo中,使用原子类型AtomicInteger替换普通int类型执行自增的原子操作,保证了线程安全。至于AtomicBoolean和AtomicLong的使用方式以及实现原理是一样,大家可以自行查阅源码。
这篇关于ava并发编程-无锁CAS与Unsafe类及其并发包Atomic的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!