(转载)Java并发编程-无锁CAS与Unsafe类

原文链接:Java并发编程-无锁CAS与Unsafe类及其并发包Atomic - CSDN博客

在前面一篇博文中,我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其原理是通过当前线程持有当前对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,也就保证了线程安全。但在本篇中,我们将会详聊另外一种反向而行的并发策略,即无锁并发,即不加锁也能保证并发执行的安全性。 

本篇的思路是先阐明无锁执行者CAS的核心算法原理然后分析Java执行CAS的实践者Unsafe类,该类中的方法都是native修饰的,因此我们会以说明方法作用为主介绍Unsafe类,最后再介绍并发包中的Atomic系统使用CAS原理实现的并发类。

无锁的概念

在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。

无锁的执行者-CAS

CAS

CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下:

执行函数:CAS(V,E,N)

其包含3个参数

1. V表示要更新的变量

2. E表示预期值

3. 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();

提供实例对象新途径。

//传入一个对象的class并创建该实例对象,但不会调用构造方法

public native Object allocateInstance(Class cls) throws InstantiationException;

类和实例对象以及变量的操作,主要方法如下:

//获取字段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);

下面通过一个简单的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的偏移量staticOffset

        long 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));

        //输出USER

        System.out.println("输出USER:"+user.toString());

        long data = 1000;

        byte size = 1;//单位字节

        //调用allocateMemory分配内存,并获取内存地址memoryAddress

        long memoryAddress = unsafe.allocateMemory(size);

        //直接往内存写入数据

        unsafe.putAddress(memoryAddress, data);

        //获取指定内存地址的数据

        long addrData=unsafe.getAddress(memoryAddress);

        System.out.println("addrData:"+addrData);

        /**

        * 输出结果:

        sun.misc.Unsafe@6f94fa3e

        staticBase: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";

    @Override

    public String toString() {

        return "User{" +

                "name='" + name + '\'' +

                ", age=" + age +'\'' +

                ", id=" + id +'\'' +

                '}';

    }

}

虽然在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;

  }

数组操作

//获取数组第一个元素的偏移地址

public native int arrayBaseOffset(Class arrayClass);

//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置

public native int arrayIndexScale(Class arrayClass);

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);

这里还需介绍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;

}

上述的方法我们在稍后的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);

内存屏障

这里主要包括了loadFence、storeFence、fullFence等方法,这些方法是在Java 8新引入的,用于定义内存屏障,避免代码重排序,与Java内存模型相关,感兴趣的可以看博主的另一篇博文全面理解Java内存模型(JMM)及volatile关键字,这里就不展开了。

//在该方法之前的所有读操作,一定在load屏障之前执行完成

public native void loadFence();

//在该方法之前的所有写操作,一定在store屏障之前执行完成

public native void storeFence();

//在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能

public native void fullFence();

其他操作

//获取持有锁,已不建议使用

@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);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342