无锁之CAS

版权声明: https://blog.csdn.net/Dongguabai/article/details/82461815

在并发控制中锁是一种策略,而无锁属于一种乐观的策略,它会假设对资源的访问时没有冲突的,没有冲突就意味着不需要等待,所有线程都可以在不停顿的状态下继续执行。如果遇到冲突无锁的使用策略就是使用CAS来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

CAS:Compare and Swap, 翻译成比较并交换。 JUC中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。

CAS算法包含三个参数:CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值(简单理解就是当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做)。

如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS会返回当前V的真实值,CAS的操作时抱着一种乐观的态度进行的,他总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会更新成功,其余均会失败。失败的线程并不会挂机,会被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没其他的线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,CAS中的比较和替换是一组原子操作,不会被外部打断,CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

AtomicInteger

在JUC中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。

在AtomicInteger中有两个很关键的属性:value是AtomicInteger当前的实际取值;valueOffset保存着value在AtomicInteger中的偏移量。

这里使用了一个static{}来初始化valueOffset,通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移,通过该偏移量valueOffset,unsafe类的内部方法可以快速获取到变量value并对其进行取值或赋值操作。

这里以addAndGet()方法为例:

扫描二维码关注公众号,回复: 3180236 查看本文章

这里使用了一个Unsafe的类:

可以看出AtomicInteger底层大量使用了Unsafe类的方法。

Unsafe(指针)

Unsafe类从名字上来看这个类的操作应该是不安全的,而我们都知道指针是不安全的。Unsafe就封装了一些类似指针的操作。而Java官方是不推荐我们使用Unsafe的,这个从Unsafe的构造方法中就可以看出来:

首先Unsafe私有了构造方法,想获取Unsafe需要调用一个静态方法getUnsafe()。我用的是JDK1.8,跟之前的版本这部分代码可能有点区别,不过大体一样的。首先要明确的是Unsafe是rt.jar里面的(具体可以参看https://blog.csdn.net/Dongguabai/article/details/82463439),根据Java类加载机制,rt.jar里面的类是由Bootstrap ClassLoader加载的,也就是说被Bootstrap ClassLoader引导的类的classLoader应该是null,不为null这里会直接抛出异常。总得来说就是Java官方不希望你直接使用Unsafe,该方法只提供给高级的Bootstrap类加载器使用,普通对象调用将抛出异常。

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值
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
public native byte getByte(long address);
public native void putByte(long address, byte x);
//操作系统的内存页大小
public native int pageSize();

//提供实例对象新途径:
//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;

//获得给定的对象偏移量上的int值
public native int getInt(Object o,long offset);

//设置给定对象偏移量上的int值
public native int putInt(Object o,long offset,int x);

//获得字段在对象中的偏移量
public native long objectFieldOffset(Field f);

//设置给定对象的int值,使用volatile语义
public native void putIntVolatile(Object o,long offset,int x);

//获得给定对象对象的int值,使用volatile语义
public native int getIntVolatile(Object var1, long var2);

//和putIntVolatile()一样,但是它要求被操作字段就是volatile的
public native void putOrderedInt(Object var1, long var2, int var4);

Unsafe与CAS相关的方法为:

//第一个参数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);

Unsafe还有挂起与恢复的方法, 将一个线程进行挂起是通过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); 

继续再看看Unsafe的getAndAddInt()方法,这个方法参数var1是当前的对象,var2是对象偏移量,var4是要增加的值:

这个方法内部调用了一个do...while语句,里面调用了一个核心方法:compareAndSwapInt()(在上面也有介绍),再简单介绍下这个方法,第一个参数var1为给定的对象,var2是对象偏移量(在上面也介绍过了,就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),var4是期望值,var5+var4是要设置的值。如果指定的字段的值等于var4,那么就会把它设置为var5:

也就是说在AtomicInteger中这个方法内部执行的就是,根据对象var1和偏移量var2得出对象实际的值var5(也就是期望值),如果底层实际值var5和对象var1的值相等(因为实际可能在执行更新操作的时候,对象的值发生了变化,所以这里要比较一下,最后就是期望的值与底层的值要完全相同,才能得出最终值,将底层的值覆盖掉),那么就会将对象var1设值为var5+var4。

猜你喜欢

转载自blog.csdn.net/Dongguabai/article/details/82461815