手撕Java原子整数类AtomicInteger -- 使用unsafe实现属于自己的原子类

导语

这个是某中大厂的一道面试题,题目内容就是不限方式实现属于自己的原子整数类,本文将阐述我在这道题中的解法,并且介绍一下Java中一个比较底层的类———Unsafe
本文适合有一定Java基础的人去看,最起码要了解一些常用的反射和并发的知识,因为我技术不咋滴,所以有很多地方讲的不到位,没有基础会看不懂,如果本文中出现什么错误欢迎大佬及时指正!

概念

俗话说得好,不管学什么都要先了解概念对吧 ---- 沃自几硕得

Unsafe类介绍

Unsafe是位于sun.misc包下的一个类,JDK中使用很多,这个类可以跨过JDK直接操作内存,从而实现更高的效率,但是这也是一把双刃剑,如果指的是使用该类不当,会造成很多莫名其妙的情况,所以被命名为Unsafe(不安全)。

什么是原子类

原子类,顾名思义,其中所有的操作都是原子性,可以保证线程安全的类,比如说原子整数(AtomicInteger ),原子引用(AtomicReference)等等… 其中的原子整数就是本文要实现自己写的那个类!
提一个疑问哈:既然之前就有Synchronized和Lock了,那为什么还要有原子类这个对象呢 ①
(答案见评论区,可以自己思考一下,不知道的话不影响继续阅读本文)

什么是CAS

CAS,即Compare And Swap 或者 Compare And Set,就是比较和替换,使用CAS可以实现无锁情况下的线程安全,实现原理就是每一次实现操作前,记录初始值后执行操作,当要对这个值更新的时候,都会和初始值比较,如果一样则进行替换,否则会自旋重试,就是常见的乐观锁实现方式。

如何获取Unsafe实例

在这里插入图片描述

在这里插入图片描述
由以上两张图可以很明确的看出来,这个类实例并不是那么好获取的,这是一个单例类,并且正常情况下 直接调用getUnsafe绝对是报错的,上述第二张图,就代表调用该方法 会判断调用类的类加载器是否为引导类加载器,如果不是的话就会抛出如下的异常

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at cn.scl.Test7.main(Test.java:46)

那么这个类实例应该如何获取呢,其实理论上来说应该是两种方式可以绕过这个安全检查,第一种是通过类加载器把调用unsafe的bootstrap class loader加载,另一种就是反射,第一种方式比较繁琐,我在评论区中写出来,好奇的可以去评论区看,文章中侧重讲解第二种。

通过反射跳过Unsafe的安全检查

  1. 拿到当前类的域对象
  2. 反射的对象在使用时应该取消 Java 语言访问检查
  3. 获取实例
	Unsafe unsafe = null;
	try {
		// 获取Unsafe类的域对象
	    Field filed = Unsafe.class.getDeclaredField("theUnsafe");
	    // 取消Java语言访问检查
	    filed.setAccessible(true);
	    // 由于我们已知Unsafe中的唯一实例是静态的,所以直接获取,拿到的就是那个实例对象
	    unsafe = (Unsafe) filed.get(null);
	} catch (NoSuchFieldException e) {
	    e.printStackTrace();
	} catch (IllegalAccessException e) {
	    e.printStackTrace();
	}
	return unsafe;

测试一下上面那一段代码
成功获取到实例

Unsafe全面的API

使用上面的方式就可以拿到Unsafe的实例了,如何详细使用不打算再本文中讲解,毕竟本文的侧重点是如何使用unsafe实现原子类,侧重点应该在原子类,贴两个API,自行查看一下吧
Unsafe的详细API
美团大佬写的使用细节

我们要实现功能需要实现的方法

上面说了unsafe类中有一堆方法,我们无需去关注太多,只要知道下面这几个方法是操作CAS的即可,就可以实现本文的最终目的,Unsafe提供的CAS方法(就是下面的方法)底层实现是CPU指令cmpxchg,所以可以实现原子性

/**
  * @param obj       要修改的对象
  * @param offset    对象中某个域的偏移量
  * @param expected  初始值
  * @param update    要更新值
  * @return          是否成功
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

这里有一个参数有可能不太好理解,就是offset这个参数,由于以上三个方法都是CPU直接操作内存的,所以就要这个偏移量,如果实在不理解,就可以简单地认为,通过这个偏移量加上一个基地址,就可以找到某个对象中某个字段的内存地址,unsafe中提供了一个方法让我们去获取对象中的域偏移量

// 获取某个对象中域的偏移量
unsafe.objectFieldOffset(ClassName.class.getDeclaredField("filedName"));

这些都拿到之后,就可以实现功能啦,接下来上代码,一起手撕AtomicInteger

亲手开发一个属于自己的原子整数操作类

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/***
 * 留下你们的赞,否则半夜删你们代码!!!!!
 * @author CG_544
 */
public class MyAtomicInteger {
    // 要操作的值
    private volatile int value;
    // unsafe实例
    private static Unsafe unsafe;
    // value的偏移量
    static long valueOffset;

    // 初始化unsafe实例以及value的偏移量
    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            // 这个异常不可能会出现,因为我们分析源码了,这个theUnsafe域 在目前jdk1.8中是绝对存在的
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }

    /***
     * 实现减法操作
     * @param value
     */
    public void sub(int value) {
        int oldValue;
        while (true) {
            oldValue = this.value;
            if (unsafe.compareAndSwapInt(this, valueOffset, oldValue, oldValue - value))
                return;
        }
    }

    /***
     * 实现加法操作
     * @param value
     */
    public void add(int value) {
        int oldValue;
        while (true) {
            // 注意这里,必须放在while中赋值,否则无法保证线程安全
            oldValue = this.value;
            if (unsafe.compareAndSwapInt(this, valueOffset, oldValue, oldValue + value))
                return;
        }
    }

    /***
     * 获取操作结果值
     * @return
     */
    public Integer getValue() {
        return value;
    }
}

class TestAtomicInteger{
    public static void main(String[] args) throws InterruptedException {
        MyAtomicInteger myAtomicInteger = new MyAtomicInteger(0);
        // 创建5000个线程 每个线程循环加10
        for (int i = 0; i < 500; i++) {
            new Thread(()->{
                myAtomicInteger.add(10);
            },"测试线程").start();
        }

        // 创建5000个线程 每个线程循环减10
        for (int i = 0; i < 500; i++) {
            new Thread(()->{
                myAtomicInteger.sub(10);
            },"测试线程").start();
        }
        Thread.sleep(2000);
        System.out.println(myAtomicInteger.getValue());
    }
}


到了这一步就大功告成,完成了属于自己的原子类,其实核心只有一个CAS操作,接下来让我们测试一下。

验证能否保证线程安全

测试方案:

  1. 假设所有线程对数据操作都是10,第一次测试使用5000个线程做加操作,5000个线程做减操作。
  2. 假设所有线程对数据操作都是10,第一次测试使用5000个线程做加操作,500个线程做减操作。
  3. 假设所有线程对数据操作都是10,第一次测试使用500个线程做加操作,50000个线程做减操作。

如果第一次测试的结果为0,第二次为45000,第三次为-45000 即为正确,见证奇迹!

第一次测试 成功!
在这里插入图片描述

第二次测试 成功!!
在这里插入图片描述
第三次 成功!!!
在这里插入图片描述

重点

  1. 本文重点理解CAS的理念
  2. 重点注意操作中 对操作数原始值赋值操作必须放在while中 否则无法保证线程安全
  3. 一键三连哈,感谢大家!!!

全面发展,一专多能,今天也是一个想进大厂的小菜鸡哈!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值