导语
这个是某中大厂的一道面试题,题目内容就是不限方式实现属于自己的原子整数类,本文将阐述我在这道题中的解法,并且介绍一下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的安全检查
- 拿到当前类的域对象
- 反射的对象在使用时应该取消 Java 语言访问检查
- 获取实例
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操作,接下来让我们测试一下。
验证能否保证线程安全
测试方案:
- 假设所有线程对数据操作都是10,第一次测试使用5000个线程做加操作,5000个线程做减操作。
- 假设所有线程对数据操作都是10,第一次测试使用5000个线程做加操作,500个线程做减操作。
- 假设所有线程对数据操作都是10,第一次测试使用500个线程做加操作,50000个线程做减操作。
如果第一次测试的结果为0,第二次为45000,第三次为-45000 即为正确,见证奇迹!
第一次测试 成功!
第二次测试 成功!!
第三次 成功!!!
重点
- 本文重点理解CAS的理念
- 重点注意操作中 对操作数原始值赋值操作必须放在while中 否则无法保证线程安全
- 一键三连哈,感谢大家!!!
全面发展,一专多能,今天也是一个想进大厂的小菜鸡哈!