Java不能像C/C++一样直接操作内存区域,需要通过本地方法的方式来操作内存区域,JDK可以通过一个后门——Unsafe类,执行底层硬件级别的CAS原子操作,线程阻塞和唤醒等。
Unsafe位于sun.misc
包下,Unsafe
类中方法几乎全部都是Native方法,它们使用JNI的方式调用本地的C++类库。
CAS是一种实现并发算法时常用的技术,自旋锁和乐观锁的实现都用到了CAS算法,JUC并发包的绝大多数工具类,如原子类AtomicInteger和重入锁ReentrantLock,他们的源码实现中都有CAS的身影。
CAS是Compare And Swap的简称,即比较再替换。它是计算机处理器提供的一个原子命令,保证了比较和替换两个操作的原子性。CAS操作涉及三个操作数:CAS(V, E, N)
CAS操作的含义是:当且仅当内存地址V中的值等于预期值E时,将内存V中的值更新为N,否则不操作(模拟CAS流程,并不是实际函数)
假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值,造成了数据不一致呢?答案是否定的,因为CAS是一条CPU原子指令,在执行过程中不允许被中断,所以不会造成所谓的数据不一致问题。
使用CAS过程中有以下三种问题:
如果一个变量V初次读取的时候是A值,那在赋值的时候检查到它依然是A值,那么是否就能说明它的值就没有被其他线程修改过了呢?这个是不能的,因为在这段时间它的值可能被改成其他值,然后又改回了A,那么CAS操作就会误认为它从来没有被修改过。这个问题就是被称为CAS操作的ABA问题。
ABA问题的产生因为变量值产生了环形更改,即一个变量的值从A改成了B,随后又从B改回了A。如果变量的值只能朝一个方向转换的话,就不会构成“环形”问题了,比如使用版本号或者时间戳机制,版本号机制是每次更改版本号变量时将版本号增加1,这样就不会存在这个问题了。JDK中的AtomicStampedReference
类使用的就是时间戳,他给每个变量配备一个时间戳,来避免ABA问题。
自旋CAS(也就是更新不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。遇到这种情况,就需要对CAS操作限制重试上限,如果重试次数达到最大值,可以通过直接退出或者采用其他方式来代替CAS。比如synchronized
同步锁,轻量级锁通过CAS自旋等待锁释放,在线程竞争激烈的情况下,自旋次数达到一定的数量时,synchronized
内部会升级为重量级锁。
CAS操作只对单个共享变量有效,当操作跨越多个共享变量时CAS无效。
Unsafe提供了很多与底层相关的操作,主要关注与CAS和线程调度相关的方法。
Unsafe unsafe = Unsafe.getUnsafe();
该方法只可以在JDK中进行使用,在其他包下,只可以通过反射进行初始化,否则会报
Exception in thread “main” java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)
这是因为 Unsafe 类主要是 JDK 内部使用,并不提供给普通用户调用,也就是其名字所暗示的那样,这些操作不安全。
public static Unsafe createUnsafe() {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Unsafe 只会分配 Person 对应的内存空间,而不触发构造函数。 注: Class 文件中的 clinit 仍然会执行。
类似方法有:putLong(Object var1, long var2, long var4)
、putOrderedLong(Object var1, long var2, long var4)
设置对象var1中内存偏移量地址var2对应的long型field的值为var4,这几个方法不同之处是,putLongVolatile
支持volatile写内存语义,保证更新对所有线程立即可见
putOrderedLong
该方法并不保证变量值的修改对其他线程立即可见
putLong
就是设置值
还有其他的类似方法,针对于其他类型,包括:int
、long
、float
、double
、byte
、char
、short
、Object
等,其中Object是针对于引用类型,这个引用类型包括Integer
等包装类。
类似方法有:getObjectVolatile(Object var1, long var2)
、getAndSetObject(Object var1, long var2, Object var4)
获取某个属性的值,其中var1是类实例,var2是offset。和putLongVolatile
类似,还有一些其他类型的类似方法。
getAndSetObject
是获取当前属性的值并且进行赋值,是一个原子操作,利用了CAS原理,并不是一个native函数,具体:
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while(!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;
}
类似的实现还有getAndAddInt(Object obj, long offset, int delta)
、getAndAddLong(Object obj, long offset, long delta)
、getAndSetInt(Object obj, long offset, int update)
、getAndSetLong(Object obj, long offset, long update)
通过 unsafe.throwExceptio 创建的异常不会被编译器检查,方法的调用者也不需要处理异常。
获取指定类中指定字段的内存偏移量地址。Unsafe可以通过类的实例和变量的偏移地址,直接读写实例对象中的值(如上面),一般用于CAS方法中。
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
通过该方法实现CAS操作,也就是 lock-free ,保证更好的性能。它的类似方法还有各种基本数据类型。
当且仅当对象obj中内存偏移量offset的field的值等于预期值expected时,将变量的值替换为uodate。替换成功返回true,否则返回false;
类似方法:long reallocateMemory(long var1, long var3)
、void freeMemory(long var1)
Java 中对象分配一般是在 Heap 中进行的(例外是 TLAB等),当应用内存不足的时候,可以通过触发 GC 进行垃圾回收,但是如果有大量对象存活到永久代,并且仍然引用可达,那么我们就需要堆外内存(Off-Heap Memory)来缓解频繁 GC 造成的压力。
Unsafe.allocateMemory
给了我们在直接内存中分配对象的能力,这块内存是非堆内存,因此,不会受到 GC 的频繁分析和干扰。
虽然这样可以缓解大量对象占用内存对 GC 和 JVM 造成的压力,这也就需要我们手动管理内存,因此,在合适的事后我们需要手动调用 freeMemory
来释放内存。
实例代码:
public class OffHeapArray {
private final static int BYTE = 1;
private long size;
private long address;
private Unsafe unsafe;
public OffHeapArray(long size, Unsafe unsafe) {
this.size = size;
this.unsafe = unsafe;
address = unsafe.allocateMemory(size * BYTE);
}
public void set(long i, byte value) {
设置指定字节的数组
unsafe.putByte(address + i * BYTE, value);
}
public int get(long idx) {
// 获取指定字节的数据
return unsafe.getByte(address + idx * BYTE);
}
public long size() {
return size;
}
public void freeMemory() {
unsafe.freeMemory(address);
}
public static void main(String[] args) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
Test t = new Test();
t.value = 1L;
Class<?> aClass = Class.forName("sun.misc.Unsafe");
Field theUnsafe = aClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE, unsafe);
int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 127);
sum += array.get((long) Integer.MAX_VALUE + i);
}
System.out.println(sum);
}
}
相关方法:void park(boolean isAbsolute, long time)
、void unpark(Object thread)
park:阻塞当前线程;
如果isAbsolute=false 且 time =0,表示一直阻塞;
如果isAbsolute=false 且 time >0,表示等待指定时间后线程会被唤醒。time 是相对时间,即当前线程在等待time毫秒后被唤醒;
如果isAbsolute=true且 time >0,表示等待指定时间后线程会被唤醒。time 是绝对时间,是某个时间点换算成相对于新纪元之后的毫秒值;
线程调用park阻塞后被唤醒时机有:
unpark:唤醒调用park后被阻塞的线程,参数thread为唤醒线程的实例
注:本文为《Java修炼指南:高频源码解析》阅读笔记