在Java中使用锁不好的地方就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文重新调度与开销。Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题。但是volatile只能保证共享变量的可见性,不能解决读-改-写的原子性问题。CAS即为Compare and Swap,是JDK提供的非阻塞的原子性操作。它通过硬件方式保证了比较-更新操作的原子性。
经典ABA问题:假如线程1使用CAS修改初始值为A的变量X(X=A),那么线程1首先会获取当前变量X的值(A),然后使用CAS操作尝试修改X的值为B,如果使用CAS修改成功了,那么程序运行一定是正常的吗?其实未必,这是因为有可能在线程1获取到变量X的值A后,在执行CAS之前,线程2使用了CAS修改了变量X值为B,然后又使用了CAS操作使得变量X值为A,虽然线程A执行了CAS操作时X=A,但是这个A已经不是线程1获取到的A了。这就是ABA问题。ABA问题的产生是因为变量的状态值产生了环形转换,就是变量值可以从A到B,也可以B到A,如果变量的值只能朝着一个方向转换,例如A到B,B到C,不构成环路,就不会存在这个问题。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题。
JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,他们使用JIN的方法访问本地C++实现库,下面我们介绍一下Unsafe类能做的事情:
long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时候使用。如下代码就是使用Unsafe类来获取变量value在AtmoicLong对象中的内存偏移。
static{ try{ valueOffset=unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value")); } catch(Exception e){ e.printStack(); } }
int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。
int arrayIndexScale(Class arrayClass)方法:获取数组中第一个元素占用的字节数。
boolean comapreAndSwapLong(Object obj,long offset,long expect,long update)方法:比较对象obj的偏移量为offset的变量值是否为expect,如果不是,则修改为update,然后返回true,否则返回false。
native long getLongvolatile(Object obj,long offset)方法:获取对象obj中偏移量为offset的变量对应的volatile语义的值。
void park()方法:阻塞当前线程。
void unpark()方法:唤醒调用park后阻塞的线程。
。。。。。。
public class TestUnsafe { //获取Unsafe实例 static final Unsafe unsafe = Unsafe.getUnsafe(); //记录变量state在类TestUnsafe中的偏移值 static long stateOffset = -1L; //变量state private volatile long state = 0; static { try { //获取state变量在TestUnsafe中的偏移值 stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state")); } catch (NoSuchFieldException e) { System.out.println(e.getLocalizedMessage()); e.printStackTrace(); } } public static void main(String[] args) { //创建实例,并设置state为1 TestUnsafe test = new TestUnsafe(); Boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1); System.out.println(success); } }
Exception in thread "main" java.lang.ExceptionInInitializerError Caused by: java.lang.SecurityException: Unsafe at sun.misc.Unsafe.getUnsafe(Unsafe.java:90) at com.heiye.temp.TestUnsafe.<clinit>(TestUnsafe.java:7)
为了找出原因,我们需要查看getUnsafe代码
@CallerSensitive public static Unsafe getUnsafe() { //2.2.7 Class var0 = Reflection.getCallerClass(); //2.2.8 if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } } //2.2.9 public static boolean isSystemDomainLoader(ClassLoader var0) { return var0 == null; }
代码(2.2.7)获取调用getUnsafe方法的Class对象,这里这个对象是TestUnsafe.class。
代码(2.2.8)判断是不是Bootsrap类加载器加载的localClass,在这里看是不是Bootsrap加载器加载了TestUnsafe.class,很明显查看是由AppClassLoader加载的,所以这里就直接抛出了异常。
为什么要进行判断呢?我们知道Unsafe类是rt.jat提供的,rt.jat包里面的类是Bootsrap类加载器加载的,而我们启动main方法的时候是AppClassLoader类加载器加载的,所以main方法在启动的时候,根据委托机制,会委托给Bootsrap加载器加载Unsafe类。如果没有代码(2.2.8)的限制,我们会可以随意的使用Unsafe类做事情了。而Unsafe类是可以直接操作内存的,是不安全的。所以JDK开发组特意做了这个限制,不让开发人员正规渠道使用Unsafe类,而是在rt.jar包的核心类中使用Unsafe类。
如果我们想用,我们可以通过反射的角度来获取Unsafe实例。
public class TestUnsafe1 { static Unsafe unsafe; static Long stateOffset; private volatile long state = 0; static { try { //使用反射来获取Unsafe成员变量theUnsafe Field field = Unsafe.class.getDeclaredField("theUnsafe"); //设置为可存取 field.setAccessible(true); //获取该变量的值 unsafe = (Unsafe) field.get(null); //获取state在TestUnsafe1中的偏移量 stateOffset = unsafe.objectFieldOffset(TestUnsafe1.class.getDeclaredField("state")); } catch (NoSuchFieldException | IllegalAccessException e) { System.out.println(e.getLocalizedMessage()); e.printStackTrace(); } } public static void main(String[] args) { TestUnsafe1 testUnsafe1 = new TestUnsafe1(); Boolean success = unsafe.compareAndSwapInt(testUnsafe1, stateOffset, 0, 1); System.out.println(success); } }
true