问题

(1)Unsafe是什么?

(2)Unsafe只有CAS的功能吗?

(3)Unsafe为什么是不安全的?

(4)怎么使用Unsafe?

简介

本章是java并发包专题的第一章,但是第一篇写的却不是java并发包中类,而是java中的魔法类sun.misc.Unsafe。

Unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。

但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的C/C++代码,但求能了解它的基本功能。

获取Unsafe的实例

查看Unsafe的源码我们会发现它提供了一个getUnsafe()的静态方法。

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

但是,如果直接调用这个方法会抛出一个SecurityException异常,这是因为Unsafe仅供java内部类使用,外部类不应该使用它。

那么,我们就没有方法了吗?

当然不是,我们有反射啊!查看源码,我们发现它有一个属性叫theUnsafe,我们直接通过反射拿到它即可。

public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
}
}

使用Unsafe实例化一个类

假如我们有一个简单的类如下:

class User {
int age; public User() {
this.age = 10;
}
}

如果我们通过构造方法实例化这个类,age属性将会返回10。

User user1 = new User();
// 打印10
System.out.println(user1.age);

如果我们调用Unsafe来实例化呢?

User user2 = (User) unsafe.allocateInstance(User.class);
// 打印0
System.out.println(user2.age);

age将返回0,因为Unsafe.allocateInstance()只会给对象分配内存,并不会调用构造方法,所以这里只会返回int类型的默认值0。

修改私有字段的值

使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。

public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null); User user = new User();
Field age = user.getClass().getDeclaredField("age");
unsafe.putInt(user, unsafe.objectFieldOffset(age), 20); // 打印20
System.out.println(user.getAge());
}
} class User {
private int age; public User() {
this.age = 10;
} public int getAge() {
return age;
}
}

一旦我们通过反射调用得到字段age,我们就可以使用Unsafe将其值更改为任何其他int值。(当然,这里也可以通过反射直接修改)

抛出checked异常

我们知道如果代码抛出了checked异常,要不就使用try...catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。

// 使用正常方式抛出IOException需要定义在方法签名上往外抛
public static void readFile() throws IOException {
throw new IOException();
}
// 使用Unsafe抛出异常不需要定义在方法签名上往外抛
public static void readFileUnsafe() {
unsafe.throwException(new IOException());
}

使用堆外内存

如果进程在运行过程中JVM上的内存不足了,会导致频繁的进行GC。理想情况下,我们可以考虑使用堆外内存,这是一块不受JVM管理的内存。

使用Unsafe的allocateMemory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受JVM管理,因此我们要调用freeMemory()方法手动释放它。

假设我们要在堆外创建一个巨大的int数组,我们可以使用allocateMemory()方法来实现:

class OffHeapArray {
// 一个int等于4个字节
private static final int INT = 4;
private long size;
private long address; private static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} // 构造方法,分配内存
public OffHeapArray(long size) {
this.size = size;
// 参数字节数
address = unsafe.allocateMemory(size * INT);
} // 获取指定索引处的元素
public int get(long i) {
return unsafe.getInt(address + i * INT);
}
// 设置指定索引处的元素
public void set(long i, int value) {
unsafe.putInt(address + i * INT, value);
}
// 元素个数
public long size() {
return size;
}
// 释放堆外内存
public void freeMemory() {
unsafe.freeMemory(address);
}
}

在构造方法中调用allocateMemory()分配内存,在使用完成后调用freeMemory()释放内存。

使用方式如下:

OffHeapArray offHeapArray = new OffHeapArray(4);
offHeapArray.set(0, 1);
offHeapArray.set(1, 2);
offHeapArray.set(2, 3);
offHeapArray.set(3, 4);
offHeapArray.set(2, 5); // 在索引2的位置重复放入元素 int sum = 0;
for (int i = 0; i < offHeapArray.size(); i++) {
sum += offHeapArray.get(i);
}
// 打印12
System.out.println(sum); offHeapArray.freeMemory();

最后,一定要记得调用freeMemory()将内存释放回操作系统。

CompareAndSwap操作

JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。

比如,我们可以基于Unsafe的compareAndSwapInt()方法构建线程安全的计数器。

class Counter {
private volatile int count = 0; private static long offset;
private static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} public void increment() {
int before = count;
// 失败了就重试直到成功为止
while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) {
before = count;
}
} public int getCount() {
return count;
}
}

我们定义了一个volatile的字段count,以便对它的修改所有线程都可见,并在类加载的时候获取count在类中的偏移地址。

在increment()方法中,我们通过调用Unsafe的compareAndSwapInt()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。

我们可以通过使用多个线程来测试我们的代码:

Counter counter = new Counter();
ExecutorService threadPool = Executors.newFixedThreadPool(100); // 起100个线程,每个线程自增10000次
IntStream.range(0, 100)
.forEach(i->threadPool.submit(()->IntStream.range(0, 10000)
.forEach(j->counter.increment()))); threadPool.shutdown(); Thread.sleep(2000); // 打印1000000
System.out.println(counter.getCount());

park/unpark

JVM在上下文切换的时候使用了Unsafe中的两个非常牛逼的方法park()和unpark()。

当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。

当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。

我们之前在分析java中的集合时看到了大量的LockSupport.park()/unpark(),它们底层都是调用的Unsafe的这两个方法。

总结

使用Unsafe几乎可以操作一切:

(1)实例化一个类;

(2)修改私有字段的值;

(3)抛出checked异常;

(4)使用堆外内存;

(5)CAS操作;

(6)阻塞/唤醒线程;

彩蛋

论实例化一个类的方式?

(1)通过构造方法实例化一个类;

(2)通过Class实例化一个类;

(3)通过反射实例化一个类;

(4)通过克隆实例化一个类;

(5)通过反序列化实例化一个类;

(6)通过Unsafe实例化一个类;

public class InstantialTest {

    private static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} public static void main(String[] args) throws Exception {
// 1. 构造方法
User user1 = new User();
// 2. Class,里面实际也是反射
User user2 = User.class.newInstance();
// 3. 反射
User user3 = User.class.getConstructor().newInstance();
// 4. 克隆
User user4 = (User) user1.clone();
// 5. 反序列化
User user5 = unserialize(user1);
// 6. Unsafe
User user6 = (User) unsafe.allocateInstance(User.class); System.out.println(user1.age);
System.out.println(user2.age);
System.out.println(user3.age);
System.out.println(user4.age);
System.out.println(user5.age);
System.out.println(user6.age);
} private static User unserialize(User user1) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
oos.writeObject(user1);
oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
// 反序列化
User user5 = (User) ois.readObject();
ois.close();
return user5;
} static class User implements Cloneable, Serializable {
private int age; public User() {
this.age = 10;
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java魔法类之Unsafe解析的更多相关文章

  1. Java魔法类:Unsafe应用解析

    本文转载自Java魔法类:Unsafe应用解析 前言 Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别.不安全操作的方法,如直接访问系统内存资源.自主管理内存资源等,这些方法 ...

  2. 死磕 java同步系列之volatile解析

    问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java ...

  3. 【java编程】Java魔法类:Unsafe应用解析

    转载来源:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html 前言 Unsafe是位于sun.mis ...

  4. 死磕 java同步系列之synchronized解析

    问题 (1)synchronized的特性? (2)synchronized的实现原理? (3)synchronized是否可重入? (4)synchronized是否是公平锁? (5)synchro ...

  5. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  6. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  7. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

  8. 死磕 java同步系列之Semaphore源码解析

    问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...

  9. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

随机推荐

  1. 【转载】TCP,IP,HTTP,SOCKET区别和联系

    网络由下往上分为:        对应 物理层-- 数据链路层-- 网络层--                       IP协议 传输层--                       TCP协议 ...

  2. Blocks实现代理传值

    一.RootViewController: #import "RootViewController.h" #import "SecondViewController.h& ...

  3. Bootstrap + Font Awesome

    将Font Awesome 集成到 Bootstrap 非常容易,还可以被单独使用. 最简单的 Bootstrap + Font Awesome 集成方式 使用这种方式将 Font Awesome 集 ...

  4. android8 Notification

     界面Layout:  customnotice.xml <?xml version="1.0" encoding="utf-8"?> <Li ...

  5. IIS application pool access desktop denied

    https://stackoverflow.com/questions/5437723/iis-apppoolidentity-and-file-system-write-access-permiss ...

  6. Android vector Path Data画图详解

    SVG是一种矢量图格式,是Scalable Vector Graphics三个单词的首字母缩写.在xml文件中的标签是,画出的图形可以像一般的图片资源使用,例子如下: <vector xmlns ...

  7. POJ - 1470 Closest Common Ancestors(离线Tarjan算法)

    1.输出测试用例中是最近公共祖先的节点,以及这个节点作为最近公共祖先的次数. 2.最近公共祖先,离线Tarjan算法 3. /* POJ 1470 给出一颗有向树,Q个查询 输出查询结果中每个点出现次 ...

  8. AutoIT: 对文件系统的菜单进行操作,有专门的语句WinMenuSelectItem

    对文件系统的菜单进行操作,有专门的语句WinMenuSelectItem: Run("notepad.exe") WinWaitActive("[CLASS:Notepa ...

  9. hdu1584(状态压缩DP)

    蜘蛛牌 Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  10. 【190】修改 PowerShell & CMD 显示字体

    方法一:Windows7更改替换cmd(powershell)字体完全方法教程 说明:该方法将字体修改成只能显示英文,对于某些中文会乱码!(chcp 850) 方法二:添加中文字体(chcp 936) ...