一个神奇的类Unsafe

引出Unsafe

如何使用Unsafe

CAS操作

不通过new,不通过反射创建对象的实例

内存分配

线程的锁操作

引出Unsafe

如果你要问我JUC包下的 AtomicInteger,AtomicLong,AtomicReference这些类是如何在并发的环境下保持原子性的,我会不假思索的回答,CAS。

以下是AtomicInteger中的一些简单常用的方法。

1 AtomicInteger ai = new AtomicInteger(9);
2 System.out.println(ai.getAndSet(8));//9
3 System.out.println(ai.getAndSet(32));//8
4 System.out.println(ai.get());//32
View Code

 AtomicInteger设置初始值为9,getAndSet(i)覆盖AtomicInteger的值为i,并返回旧值。get()获取当前值。

1 AtomicInteger ai = new AtomicInteger(14);
2 System.out.println(ai.getAndAdd(2));//14
3 System.out.println(ai.addAndGet(5));//21
4 System.out.println(ai.get());//21
View Code

getAndAdd(i)在原有的值上增加i,并返回旧值。addAndGet(i),在原有的值上增加i,并返回增加后的值。

1 AtomicInteger ai = new AtomicInteger(14);
2 System.out.println(ai.getAndIncrement());// 14
3 System.out.println(ai.incrementAndGet());// 16
4 System.out.println(ai.get());// 16
View Code

getAndIncrement()原有的值自增1,并返回旧的值。incrementAndGet(i)原有的值自增1,并返回自增后的值。

AtomicInteger ai = new AtomicInteger(14);
System.out.println(ai.getAndDecrement());// 14
System.out.println(ai.decrementAndGet());// 12
System.out.println(ai.get());// 12
View Code

getAndDecrement()原有的值自减1,然后返回旧的值。decrementAndGet()原有的值自减1,然后返回新的值。

1 AtomicInteger ai = new AtomicInteger(14);
2 System.out.println(ai.compareAndSet(14, 16));// true
3 System.out.println(ai.compareAndSet(14, 18));// false
4 System.out.println(ai.get());// 16
View Code

compareAndSet(x,y)如果原值为x,则替换为y,返回true或者false。

 1 AtomicInteger ai = new AtomicInteger(14);
 2 
 3 System.out.println(ai.getAndSet(8));// getAndSetInt
 4 
 5 System.out.println(ai.getAndAdd(2));// getAndAddInt
 6 System.out.println(ai.addAndGet(5));// getAndAddInt
 7 
 8 System.out.println(ai.getAndIncrement());// getAndAddInt
 9 System.out.println(ai.incrementAndGet());// getAndAddInt
10 
11 System.out.println(ai.getAndDecrement());// getAndAddInt
12 System.out.println(ai.decrementAndGet());// getAndAddInt
13 
14 // compareAndSwapInt
15 System.out.println(ai.compareAndSet(14, 16));
View Code

下面来看AtomicInteger的源代码

 private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
View Code

源码中的unsafe,valueOffset,value是关键的成员变量。

value是再构造时指定的值,AtomicInteger原子类操作的也就是这个value。

AtomicInteger中大部分方法都依赖与unsafe。

valueOffset使用unsafe.objectFieldOffset(Field);获取value成员变量相对Java对象的“起始地址”的偏移量。这句话我自己的理解是再Class对象中的偏移量。(具体待考证!)

getAndAdd,addAndGet,getAndIncrement,incrementAndGet,getAndDecrement,decrementAndGet这六个方法最终都是调用的UnSafe的getAndAddInt(Object,valueOffset,value)。

getAndSet则是调用的getAndSetInt(Object,valueOffset,newValue)。

compareAndSet则是调用的compareAndSwapInt(Object,valueOffset,expect,update);

 public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
View Code

其中getAndAddInt,getAndSetInt最终调用的都是compareAndSwapInt。所以这个方法才是我们最终的目标。它的参数如下:

Object:该实例对象为目标对象。

valueOffset:对Object中的这个字段做CAS操作。

expect:期望值

newValue:替换值。

这个方法是native方法它的作用是,当Object中的valueOffset字段值为expect则替换为newValue。

在getAndAddInt,getAndSetInt这两个方法中都调用了getIntVolatile(Object, valueOffset);查询原值,将查询到的原值当作expect。

以getAndAddInt为例,先获取原值,当作expect,然后进行CAS操作如果失败则肯定有其他线程已经替换了值。然后使用do{}while();的方式自旋调用compareAndSwapInt,直到成功替换。

如何使用Unsafe

现在我们的目标是不通过AtomicInteger类自己使用Unsafe,首先要获取一个Unsafe的实例。

 private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
View Code

很遗憾,因为构造器是私有的,我们无法通过构造器去实例化它。不过有静态的getUnsafe()通过它可以获取一个实例。在AtomicInteger中也是这么做的。

不过当我使用Unsafe unsafe = Unsafe.getUnsafe();得到的确是一个异常。Exception in thread "main" java.lang.SecurityException: Unsafe。

再次阅读源码发现该静态方法会判断引用此方法的对象如果不是引导类加载器则会抛出异常。根据双亲委派模型,AtomicInteger类肯定是引导类加载器创建的,而我自己的测试类则使用的是应用类加载器。

在这里我是想过如何使用引导类加载器去加载我的测试类这个问题,但是无果。无奈只能使用反射这种十分无赖的方式拿到Unsafe。

public class DemoApplication {

    private int age = 22;

    public static void main(String[] args) {

        try {
            DemoApplication t = new DemoApplication();

            System.out.println(t.age);//打印原始值 22

           // Class<?> clazz = Class.forName("sun.misc.Unsafe");
            
            Class<?> clazz = DemoApplication.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            long fieldOffset = unsafe.objectFieldOffset(DemoApplication.class.getDeclaredField("age"));

            System.out.println(fieldOffset);//打印偏移量 12

            boolean compareAndSwapLong = unsafe.compareAndSwapInt(t, fieldOffset, 22, 44);

            System.out.println(compareAndSwapLong);//是否交换成功 true

            System.out.println(t.age);//交换成功后的age 44

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
View Code

class.forName和classLoader.loadClass两个方法都可以获取Class对象,区别是class.forName在加载类时或触发初始化,而classLoader.loadClass则不会触发初始化。因为Unsafe对象中没有静态代码块(在类加载中的初始化步骤会触发静态代码块)。所以使用

哪种方式获取Class都可以。接着使用反射获取构造器。为什么不直接clazz.newInstance()呢,因为newInstance是要走构造器的,私有的构造器是无法获取实例的。记得添加暴力破解setAccessible(true);

接下来就要使用compareAndSwapInt,偏移量的获取方式按照AtomicInteger中的做,创建目标对象,我们的期待值是22,要替换成44。

CAS操作

public class DemoApplication2 {

    public Users old = new Users(1, "张三", 22);

    public static void main(String[] args) {
        try {

            DemoApplication2 demoApplication2 = new DemoApplication2();

            Class<?> clazz = DemoApplication2.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            Field field = Class.forName("com.dfsn.cloud.consumer.DemoApplication2").getField("old");

            boolean cas = unsafe.compareAndSwapObject(demoApplication2, unsafe.objectFieldOffset(field), demoApplication2.old, null);

            System.out.println(cas);//true

            System.out.println(demoApplication2.old==null);//true

            boolean cas2 = unsafe.compareAndSwapObject(demoApplication2, unsafe.objectFieldOffset(field), null, new Users(2,"李四",33));

            System.out.println(cas2);//true

            System.out.println(demoApplication2.old.getName());//李四

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
View Code

上边代码片段使用compareAndSwapObject做CAS交换内存操作。先讲成员变量old更改为null,然后再更改为一个新的对象。

不通过new,不通过反射创建对象的实例

public static void main(String[] args) {
        try {

            DemoApplication2 demoApplication2 = new DemoApplication2();

            Class<?> clazz = DemoApplication2.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            Users users = (Users) unsafe.allocateInstance(Class.forName("com.dfsn.cloud.consumer.Users"));

            System.out.println(users == null);//false

            users.setName("哈哈");

            System.out.println(users.getName());//哈哈

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
View Code
   public static void main(String[] args) {
        try {

            DemoApplication2 demoApplication2 = new DemoApplication2();

            Class<?> clazz = DemoApplication2.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            Users users = (Users) unsafe.allocateInstance(Class.forName("com.dfsn.cloud.consumer.Users"));

            long fieldOffset  = unsafe.objectFieldOffset(Users.class.getDeclaredField("name"));

            unsafe.putObject(users,fieldOffset,"哈哈哈哈哈哈哈哈");

            System.out.println(users.getName());//哈哈哈哈哈哈哈哈

            users.setName("喜喜喜喜喜喜");

            System.out.println(users.getName());//喜喜喜喜喜喜

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
View Code

这个方法又一次刷新了我的知识储备。原来除了new反射还有其他创建对象的方法,并且操作对象的属性 -.-

内存分配

public class DemoApplication2 {

    public static void main(String[] args) {
        try {

            DemoApplication2 demoApplication2 = new DemoApplication2();

            Class<?> clazz = DemoApplication2.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            long address = unsafe.allocateMemory(4);

            unsafe.putInt(address,10);

            System.out.println(unsafe.getInt(address));//10

            long address2 = unsafe.allocateMemory(1);

            unsafe.putInt(address2,10);

            System.out.println(unsafe.getInt(address2));//10 

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
View Code

这个玩法就有些高端了,自己规划内存大小,往内存中设置值。allocateMemory()的参数为byte的大小,第一次我划分了4个字节,存入一个int类型刚好放满,第二次划分1个字节,按道理应该分配赋值失败的,但是成功了,有些疑惑。

线程的锁操作

  public static void main(String[] args) {
        try {

            DemoApplication2 demoApplication2 = new DemoApplication2();

            Class<?> clazz = DemoApplication2.class.getClassLoader().loadClass("sun.misc.Unsafe");

            Constructor<?> constructor = clazz.getDeclaredConstructor();

            constructor.setAccessible(true);

            Unsafe unsafe = (Unsafe) constructor.newInstance();

            Thread t = new Thread(() -> {
                unsafe.park(true, System.currentTimeMillis() + (1000 * 10));

                System.out.println("线程1~~~~~~~~~~~~~~~~~~~~~~~");
            });

            t.start();

            Thread.sleep(1000 * 5);

            unsafe.unpark(t);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
View Code

而park()和unpark()分别对应Lock和unLock,以上代码片段,表示线程会在当前时间的10秒后自动释放锁,如果调用unpark则立刻释放锁。

 

posted @ 2021-04-22 10:30  顶风少年  阅读(72)  评论(0编辑  收藏  举报
返回顶部