一直以来, JVM作为一个自动内存管理的运行时环境, 使得我们Java程序员不必像隔壁C/C++程序员那样, 操心内存的申请和释放, 摆脱了大部分因为疏忽内存管理而造成的内存泄露问题, 但是Java是否就不具备直接管理内存的能力呢?
今天我们就上手一段小程序: 多线程并发实现最常见的自增操作, 在保证线程安全的前提下.以此来实践演示一下Java中直接操作内存的技术: Unsafe.
并阐述并发编程中一个很重要的操作: CAS
需求:
1, 向线程池提交10个Task;
2, Task内容: 各自对一个共享int变量num自增1000次;
3, 期望的结果: num = 10*1000 = 10000
实现手段: 为了保证线程安全, 手段有很多, 最容易想到的就是加锁来保证操作的原子性和可见性, 例如synchronized内置互斥锁.
注: i++,++i是非原子操作, 看似只有一行Java代码, 但是其底层的汇编指令至少为3条(读值, 计算, 存值), 多线程下很容易破坏原子性导致结果错误.
新版本的synchronized(JDK5以来)有一个锁升级的过程(旧版本的就更不比说了, 直接是重量级锁), 在多线程激烈竞争下, 会经过偏向锁, 升级为轻量级锁, 最终甚至升级成为OS级别的重量级锁, 一旦到了重量锁, 多线程的阻塞和唤醒, 涉及用户态到内核态的切换, 导致该锁使用起来开销大, 性能低.(涉及Linux内存系统调用内容, 不懂的请自行了解).
而今天介绍的Unsafe通过封装了OS的cas支持, 使我们在并发程序中实现无锁操作, 屏蔽掉重量级锁的弊端.
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Instruction: 通过Unsafe实现无锁自增并测试结果.
* Author:@author MaLi
*/
public class T02_UnsafeUsage {
//通过unsafe在多线程中修改其属性值, 最后打印最终结果
static class OptimisticLocking {
// 待自增的整数共享变量
private volatile int num = 0; // 此处要保证可见性, 所以加上volatile
// 主角: 使用Unsafe直接操作内存
private static Unsafe unsafe;
// 内存的地址: 这里使用相对实例对象的偏移量
private static long valueOffset;
static {
//初始化
try {
// 在字节码中获取属性
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
// 关键1: 获取Unsafe实例
unsafe = (Unsafe) theUnsafe.get(null);
// 关键2: 获取要操作属性的address - 偏移量
valueOffset = unsafe.objectFieldOffset(OptimisticLocking.class.getDeclaredField("num"));
//如果是静态变量 unsafe.staticFieldOffset(静态变量);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
//使用Unsafe的cas操作, 修改num的值
public final boolean unSafeCompareAndSet(int oleValue, int newValue) {
//关键操作: 使用Unsafe的cas
return unsafe.compareAndSwapInt(this, valueOffset, oleValue, newValue);
}
//自旋方式自增
public void selfPlus() {
int oldValue = num;
do {
//获取当前最新num值
oldValue = num;
} while (!unSafeCompareAndSet(oldValue, oldValue + 1));
}
}
// 测试用例: 提交10个并行Task, 每个Task使用自造的selfPlus实现自加1000次, 期望的结果是1万
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(10);
OptimisticLocking optimisticLocking = new OptimisticLocking();
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
service.submit(() -> {
for (int j = 0; j < 1000; j++) {
optimisticLocking.selfPlus(); //调用自造的Api实现无锁自增
}
latch.countDown(); //10个线程总共倒数10个数
});
}
try {
latch.await(); //让MainThread在此等待所有Task执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(optimisticLocking.num); //期望的结果: 10000
service.shutdownNow(); //暴力关闭线程池
}
}
总结: 本文代码主要为了实现原子自增,来了解JUC中原子类的实现原理
关键点是通过对volatile修饰的变量实现CAS+自旋操作.
自旋的目的: 如果没有通过CAS设置value成功, 就获取当前value值, 重新CAS设置.
另外, 其实在java并发包中提供了很多原子类, 作用是在并发编程中, 实现线程安全的原子操作 - 自增,自减, CAS设置值等. 这些内容属于原子类的使用问题, 用起来很简单, 关键是其底层原理都使用了我们今天介绍的Unsafe的CAS操作. 下面列举部分源码作为说明
Step1: AotmicInteger的自增1
–> java.util.concurrent.atomic.AtomicInteger
关键点1: 调用Unsafe的函数
// 该函数的作用类似: i++, 即先获取值, 再做运算
public final int getAndDecrement() {
return U.getAndAddInt(this, VALUE, -1);
}
Step2: Unsafe的getAndAddInt函数调用
–> jdk.internal.misc.Unsafe
//
/**
* Object o: 要操作的变量所属实例
* long offset: 待操作的变量在所属对象中的偏移量, 借此+ Object o计算出该变量在内存中的位置
* int delta:
*/
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v; // 创建一个变量保存当前变量的值
do {
v = getIntVolatile(o, offset); // 获取变量最新值(可能是其它线程改变过的)
} while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 如果没有修改成功, 继续, 直到自增完成
return v; // 返回修改前的值, 也就是i++的i
}
–> jdk.internal.misc.Unsafe
@HotSpotIntrinsicCandidate
/**
* Object o : 要修改的变量所在对象实例
* long offset: 变量相对其所属对象的偏移量
* int expected: 期望内存中的值
* int x: 修改为目标值
*/
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
–> jdk.internal.misc.Unsafe
@HotSpotIntrinsicCandidate
// 这是一个native方法, 也就是用C++代码对OS中CAS的封装
/**
* Object o : 要修改的变量所在对象实例
* long offset: 变量相对其所属对象的偏移量
* int expected: 期望内存中的值
* int x: 修改为目标值
*/
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
除去Unsafe这些关键的函数, 一定不要忘记还有一个关键点:
关键点2: AtomicInteger中一行代码
private volatile int value;
就是其中这个value值必须为volatile的, 加volatile关键字的原因, 多线程操作共享的变量value, CAS只是保证计算, 但前提是各个线程在各自CPU缓存里要看见彼此计算之后对value的修改, 也就是保证线程之间的可见性, 所以必须加上它.
至此,通过自己手写了一个自增原子操作, 另外解析了一个原子类自增源码. 原子类的使用, 非常简单就不多罗列了.
后续想到继续添加.
关于本文, 有疑问欢迎通过留言交流.