Unsafe -- Java的魔法类(二)
原创不易,转载请注明来源
文接上集,上集主要是对Unsafe类做了简单的介绍,从本问开始,主要是对Unsafe类的八大类的详细用法展开演述。
1、CAS
- java.util.concurrent.atomic相关类
- Java AQS
- ConcurrentHashMap
CAS 即compare and swap,中文名被翻译为比较并交换。在sun.misc.Unsafe类中,主要方法体现为一下三种:
public final boolean compareAndSwapObject(Object o, long offset, Object expected, Object x)
public final boolean compareAndSwapInt(Object o, long offset, int expected, int x)
public final boolean compareAndSwapLong(Object o, long offset, long expected, long x)
- 第一个参数o:当前的对象。
- 第二个参数offset:内存地址。
- 第三个参数expected:被交换的值。
- 第四个参数x:交换的值。
解释:该方法的通过offset内存地址与被交换的值进行比较,如果他们相等,则将值替换为x,并返回true;
感觉听起来有点抽象的话,请看下面的案例:
案例: int a = 1; 先通过CAS的方式,将a的值变为2。
public class CasDemo {
public static void main(String[] args) {
CasDemo casDemo = new CasDemo();
boolean flag = casDemo.compareAndSwap(1, 2);
System.out.println(flag+", a="+casDemo.getA());
}
public volatile int a = 1;
private long offset;
public int getA() {
return a;
}
/**
*
* @param curr 当前本应的值
* @param expect 期望变为的值
*/
@SneakyThrows
public boolean compareAndSwap(int curr, int expect){
Unsafe unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(this.getClass().getField("a")); //获取变量a的内存地址
return unsafe.compareAndSwapInt(this,offset,curr,expect);
}
@SneakyThrows
Unsafe getUnsafe() {
Field declaredField = Unsafe.class.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
return (Unsafe) declaredField.get(null);
}
}
运行结果:
有人会想,不就是换个值嘛,有必要搞得这么花里胡哨吗?
细想来,如果是单线程少并发情况下,确实没必要,直接进行赋值a++就好了;但如果是高并发情况呢?有人说可以加锁啊!直接使用synchronized关键字进行加锁,不可否认确实可以解决问题,但是直接上来使用一个重量级锁,不感觉屈才了吗?也确实没必要,细想来也有一个线程安全的类AtomicInteger可以完成此任务,直接调用其方法 getAndIncrement() 默认向上加一。点开看其源码,如下:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
return compareAndSetInt(o, offset, expected, x);
}
原来也还是通过调用Unsafe类的CAS方法进行操作,但是考虑到在多线程情况下,假设现在共享变量a=1,有两个线程t1和t2去获取修改共享变量a,线程t1欲使共享a的值加1,线程t2也欲使共享a的值加1。t1跑的比较快,先获取到a的值为1,随后t2也获取到a的值为1,紧接着t1拿自己副本o(值为1)与真实的o(即a的值,值为1)比较,相等,修改成功,t2也拿自己副本o(值为1)与真实的o(即a的值,值为2)比较,不相等,修改失败;
案例:main线程和线程t1修改共享变量a,main线程先拿到值1,但并未写回,线程t1获取到假设为主线本应写回的数据1,欲写回数据2。
public class CasDemo03 {
@SneakyThrows
public static void main(String[] args) {
CasDemo03 casDemo = new CasDemo03();
new Thread(() -> {
System.out.println("等待。。。");
int andSwap = casDemo.compareAndSwap(1, 2);
System.out.println(andSwap);
}).start();
Thread.sleep(3000);
casDemo.setA(1);
}
public volatile int a = 0;
private long offset;
private Unsafe unsafe = getUnsafe();
public Integer getA() {
return a;
}
@SneakyThrows
public void setA(Integer a) {
offset = unsafe.objectFieldOffset(this.getClass().getField("a"));
this.a = a;
}
//获取变量a的内存地址
@SneakyThrows
public long getOffset() {
offset = unsafe.objectFieldOffset(this.getClass().getField("a"));
return offset;
}
@SneakyThrows
public int compareAndSwap(Integer curr, Integer expect) {
offset = getOffset();
do {
} while (!unsafe.compareAndSwapInt(this, offset, curr, expect));//如果判断失败,则自旋等待
return getA();
}
@SneakyThrows
private final static Unsafe getUnsafe() {
Field declaredField = Unsafe.class.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
return (Unsafe) declaredField.get(null);
}
}
可以很容易的发现线程t1会先打印出等待。。。
,直到main线程修改共享变量为1之后才继续运行。其实在此处,大家可能也很容易就发现一个问题,假使在t1循环判断期间,有一个线程偷偷将共享变量a变为其他值,然后再偷偷变回1,线程t1是绝对发现不了的,这也就是我们常说的多线程的ABA问题。解决ABA主流就是引入版本号(这也是jdk源码中解决ABA问题的方式),从Java1.5 开始,JDK 的 atomic 包里提供了一个类 AtomicStampedReference 用来解决 ABA 问题。详细请看AtomicStampedReference源码中的compareAndSet()
方法。