Java与C语言一个较大差别是JVM屏蔽底层细节,使我们开发能够更专注于业务实现。
Unsafe类,顾名思义就说非安全类,属于sun.misc包下,是java开放给用户直接接触底层。网上大部分文章大部分直接讲解API,楼主这篇文章是通过应用场景去讲解Unsafe类,并且每个用途都附上使用用例,不仅教会你懂原理,还让你会使用。但还是由衷告诫你,除非是用来开发基础框架,否则不推荐使用。
在ConcurrentHashMap的源码中,我们可以看到Unsafe的示例化方法
private static final sun.misc.Unsafe U;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
} catch (Exception e) {
throw new Error(e);
}
}
但实际使用这段代码进行示例化时,会抛出非安全异常,这是神马操作?
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
点进去看源码,原来是类加载问题。JUC包属于rt.jar,是由根类加载器进行加载,因此不会报错。对类加载不熟的同学可以参考《深入浅出JVM系列1:类加载器及其用法》
构造函数私有化
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
//获取调用的类
Class var0 = Reflection.getCallerClass();
// 根类加载器一般使用null来表示,而自定义类的类加载器为应用类加载器,因此两者不相等
if (var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
通过getUnsafe方法获取theUnsafe属性这条路行不通,那么我们只能使用必杀技—反射。
public class UnsafeApp {
private static Unsafe unsafe;
/**
* 获取Unsafe类的一个实例
*/
static {
try {
//getDeclaredFiled 获取类本身的属性成员
Field f = Unsafe.class.getDeclaredField("theUnsafe");
//私有对象必须设置为true,否则会报错
f.setAccessible(true);
unsafe = (Unsafe)f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
提到操作对象的私有属性,基本会使用反射。我们通过Unsfa类的objectFieldOffset方法也能实现获取对象的私有属性
/**
* 学生类
*/
public class Student {
//编号
private int id;
//姓名
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class UnsafeApp {
...省略unsafe示例方法...
public static void main(String[] args) throws Exception{
/*
* 操作私有对象属性
*/
Student student = new Student(); //unsafe修改私有变量
Field field = student.getClass().getDeclaredField("id");//getDeclaredField可以获取私有的变量
//方法1:采用反射修改私有变量
field.setAccessible(true); //为true时可以访问私有类型变量
field.set(student, 111);
System.out.println("student.id: " + student.getId());
}
//方法2:采用unsafe修改私有变量
unsafe.putInt(student, unsafe.objectFieldOffset(field), 20);
System.out.println("student.id: " + student.getId());
}
打印结果
student.id: 111
student.id: 20
JVM通过垃圾回收机制管理Java内存,使我们不需要像C语言一样,每次使用完堆内存还要自己手动释放,否则会造成内存泄漏。Java其实也是可以操作JVM外的内存,称为堆外内存或直接内存,是通过Unsafe实现。
用Unsafe开辟的内存空间不占用JVM Heap空间,也不具有自动内存回收功能,需要自己手动释放。典型的用途是本地缓存缓存。本地缓存相等Redis等内存数据库更为方便快捷,但会给虚拟机带来GC压力,因此可以通过堆外内存将数据缓存起来,如Ehcache。
/**
* 操作堆外内存
*/
byte[] bigObject = "大对象".getBytes();
//分配堆外内存
//返回对外内存的地址
long address = unsafe.allocateMemory(bigObject.length);
//添加元素到指定位置
for (int i = 0; i < bigObject.length; i++) {
unsafe.putByte(address + i, bigObject[i]);
}
//获取指定位置的元素
byte[] byteArray = new byte[bigObject.length];
for (int i = 0; i < bigObject.length; i++) {
byteArray[i] = unsafe.getByte(address + i);
}
System.out.println(new String(byteArray));
打印结果
大对象
JDK1.6之前,多线程抢占资源时,使用synchonized关键字会触发系统调用,让没抢到锁的资源进入阻塞状态,后面获得资源后才恢复为RUNNABLE状态,这个操作过程涉及到用户态和内核态的切换,代价比较高。CAS(Compare And Swap,比较并替换)是一个原子操作,需要CPU支持。CAS更新一个变量的时候,只有当变量的预期值expect和地址偏移量offset当中的实际值相同时,才会将offset对应的expect修改为update。
*
* @param obj 需要更新的对象
* @param offset obj中内存地址的偏移量
* @param expect 旧的值
* @param update 期望值
* @return 更新成功返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
CAS是原子操作,JUC包中并发容器中的底层很多就是使用CAS,如AtomicInteger等,被广泛用于并发编程。
这里模拟两个计数器,一个非线程安全,一个使用CAS保证线程安全
/**
* 非安全版本计数器
*/
public class UnSafeCounter {
private int count = 0;
public void increment() {
count++;
}
public int get() {
return count;
}
}
/**
* 线程安全版本计数器
*/
public class SafeCounter {
private volatile int count = 0;
private final static long valueOffset;
private final static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
valueOffset = unsafe.objectFieldOffset
(SafeCounter.class.getDeclaredField("count"));
} catch (Exception ex) { throw new Error(ex); }
}
public void increment() {
//无限循环
for (;;) {
int current = get();
int next = current + 1;
//CAS操作
if (unsafe.compareAndSwapInt(this, valueOffset, current, next))
//原子更新成功,跳出无限循环
break;
}
}
public int get() {
return count;
}
}
// 测试代码
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
0,
TimeUnit.SECONDS,
new LinkedBlockingQueue());
int count = 10 * 10000;
for(int i = 0; i < count; i++)
executor.execute(new Runnable() {
@Override
public void run() {
unSafeCounter.increment();
}
});
Thread.sleep(5000);
System.out.println("UnSafeCounter.count: " + unSafeCounter.get());
for(int i = 0; i < count; i++)
executor.execute(new Runnable() {
@Override
public void run() {
safeCounter.increment();
}
});
Thread.sleep(5000);
System.out.println("SafeCounter.count: " + safeCounter.get());
//关闭线程池
executor.shutdown();
}
打印结果:
UnSafeCounter.count: 99996
SafeCounter.count: 100000
这里有个大坑,线程池的getActiveCount()不准确问题,还是使用sleep方法,把时间设置长一点从而保证没有活跃线程
while(executor.getActiveCount() != 0){
Thread.sleep(1000);
}
在并发环境下,线程A和线程B通过CAS在争夺资源,A通过CAS成功将预期值expect改为update后,此时,其他任务的线程C又将update改为expect,这时候B会误以为自己抢占到资源,从而产生脏数据,这就是ABA问题。
网上最常见的解决方法是加个版本号,每次将预期值expect改为update后,版本号就会发生变化。
public class AtomicStampedReferenceApp {
public class AtomicStampedReferenceApp {
public static void main(String[] args) throws Exception {
String expert = "gz";
String update = "sz";
int initversion = 1;
AtomicStampedReference reference = new AtomicStampedReference(expert,1);
//从gz去sz,现在又回到gz
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
reference.compareAndSet(expert,update,initversion,reference.getStamp()+1);
System.out.println("从"+ expert + "去" + reference.getReference() + ",现在的版本号:" + reference.getStamp());
reference.weakCompareAndSet(update,expert,reference.getStamp(), reference.getStamp()+1);
System.out.println("现在回到" + reference.getReference() + ",现在的版本号:" + reference.getStamp());
}
});
t1.start();
Thread.sleep(1000);
//不使用版本做限制,存在aba问题
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean ret = reference.compareAndSet(expert,update, reference.getStamp(),reference.getStamp()+1);
System.out.print(ret? "我没有离开gz" : "我有离开过gz" );
System.out.println(",现在的版本号:" + reference.getStamp());
}
});
t2.start();
Thread.sleep(1000);
//使用指定版本号,发现已经发生变化,CAS更新失败,解决了aba问题
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean ret = reference.compareAndSet(expert,update, initversion,reference.getStamp()+1);
System.out.print(ret? "我没有离开gz" : "我有离开过gz" );
System.out.println(",现在的版本号:" + reference.getStamp());
}
});
t3.start();
}
打印结果
从gz去sz,现在的版本号:2
现在回到gz,现在的版本号:3
我没有离开gz,现在的版本号:4
我有离开过gz,现在的版本号:4
如果看过AQS源码,里面还提供一种解决ABA的方法。A,B,C三个线程执行CAS,线程A抢占成功将同步状态从0更新为1,失败的线程被放到FIFO队列中。
后面A处理完将同步状态更新回0,如果此时刚好有线程D执行CAS,不就会获得资源,相当于插队,这不就堆B和C不公平?AQS提供的解决方法是判断队列中是否有线程在排队,如果有则不执行CAS。
JUC中,当一个线程需要等待某个操作时,通过Unsafe的park()方法来阻塞此线程。当线程需要再次运行时,通过Unsafe的unpark()方法来唤醒此线程。如LockSupport.park()/unpark(),它们底层都是调用的Unsafe的这两个方法。
//阻塞线程
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
//唤醒线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
根据源码,我们可以模拟线程的阻塞和唤醒
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
System.out.println("线程" + Thread.currentThread().getName() + "开始阻塞");
//第一个参数为true时,第二个参数使用单位为毫秒的绝对时间,表示要阻塞到某个时间点,如unsafe.park(true, System.currentTimeMillis() + 1000)表示阻塞1秒
//第一个参数为false时,第二个参数使用单位为纳秒的绝对时间,表示要阻塞的时间间隔, 0L表示永久阻塞,如 unsafe.park(false, 1000000000l)表示阻塞1秒
unsafe.park(false, 0L);
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
long endTime = System.currentTimeMillis();
System.out.println("阻塞时间为:" + (endTime - startTime) / 1000 + "秒");
}
});
t1.setName("park_test_thread");
t1.start();
Thread.sleep(3000);
unsafe.unpark(t1);
打印结果
线程park_test_thread开始阻塞
线程park_test_thread被唤醒
阻塞时间为:3秒
在调用park()之前调用了unpark或者interrupt则park阻塞失效。
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// unsafe.unpark(Thread.currentThread());
Thread.currentThread().interrupt();
long startTime = System.currentTimeMillis();
System.out.println("线程" + Thread.currentThread().getName() + "开始阻塞");
unsafe.park(false, 0L);
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
long endTime = System.currentTimeMillis();
System.out.println("阻塞时间为:" + (endTime - startTime) / 1000 + "秒");
}
});
t1.setName("park_test_thread");
t1.start();
Thread.sleep(3000);
unsafe.unpark(t1);
打印结果
线程park_test_thread开始阻塞
线程park_test_thread被唤醒
阻塞时间为:0秒
先了解下可见性的使用场景
public class CounterThread extends Thread {
private Object isContinue = "true";
private final static long valueOffset;
private final static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
valueOffset = unsafe.objectFieldOffset
(CounterThread.class.getDeclaredField("isContinue"));
} catch (Exception ex) { throw new Error(ex); }
}
public void setContinue(Object isContinue) {
this.isContinue = isContinue;
}
public Object isContinue() {
return isContinue;
}
@Override
public void run() {
System.out.println("开始统计");
while (isContinue == "true")
{
}
System.out.println("结束统计");
}
public class TestCounter {
public static void main (String[] args) throws Exception{
CounterThread thread = new CounterThread();
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.setContinue("false");
}
}
打印结果
开始统计
线程一直处于运行状态,是因为线程每次都是从私有堆栈中拿取isContinue的值,而不是从公共堆栈中获取,导致isContinue的值发生改变,但是线程没响应。
使用Unsafe中的getObjectVolatile方法,线程会立即获取公共堆栈中最新的值。
public void setContinue(Object isContinue) {
this.isContinue = isContinue;
}
@Override
public void run() {
System.out.println("开始统计");
while (unsafe.getObjectVolatile(this, valueOffset) == “true”)
{
}
System.out.println("结束统计");
}
打印结果
开始统计
结束统计
Unsafe通过获取数组的起始偏移量和每个元素空间大小,从而直接操作数组,其中结合位运算的方法比乘法运算的效率会更高。
int[] array = new int[20];
//数组第一个元素的偏移地址
int baseOffset = unsafe.arrayBaseOffset(array.getClass());
//数组中每个元素的空间大小
int indexScale = unsafe.arrayIndexScale(array.getClass());
for (int i = 0; i < array.length; i++){
//数组中第i个元素的偏移量
//方法1 基于乘法运算
// long offset = baseOffset +(i * indexScale);
//方法2 基于位运算
//numberOfLeadingZeros表示从最左边开始数起连续的0的个数为,如 numberOfLeadingZeros(1)的值为31
int ssfit = 31 - Integer.numberOfLeadingZeros(indexScale);
long offset = (i << ssfit) + baseOffset;
unsafe.putInt(array, offset, i);
}
for (int i = 0; i < array.length; i++){
System.out.println("元素位置:" + i + " 值:" + array[i]);
}
打印结果
元素位置:0 值:0
元素位置:1 值:1
元素位置:2 值:2
元素位置:3 值:3
元素位置:4 值:4
其中这段代码比较难理解。那么它具有普适性吗?答案是否。
int ssfit = 31 - Integer.numberOfLeadingZeros(indexScale);
long offset = (i << ssfit) + baseOffset;
先看下测试用例
int num1 = 7;
System.out.println(1 << (31 - Integer.numberOfLeadingZeros(num1)));
System.out.println(1 * num1);
int num2 = 8;
System.out.println(1 << (31 - Integer.numberOfLeadingZeros(num2)));
System.out.println(1 * num2);
打印结果
4
7
8
8
从结果可以看出这段代码不具备普适性,有且只有当num为2^n时才能成立。
这就好理解,即一个数i * 2^n等价于 i << n。