在前面一篇博文中,我们曾经详谈过有锁并发的典型代表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();
提供实例对象新途径。
//传入一个对象的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;
}
网友评论