一直以来, 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;
public class T02_UnsafeUsage {
static class OptimisticLocking {
private volatile int num = 0;
private static Unsafe unsafe;
private static long valueOffset;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
valueOffset = unsafe.objectFieldOffset(OptimisticLocking.class.getDeclaredField("num"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public final boolean unSafeCompareAndSet(int oleValue, int newValue) {
return unsafe.compareAndSwapInt(this, valueOffset, oleValue, newValue);
}
public void selfPlus() {
int oldValue = num;
do {
oldValue = num;
} while (!unSafeCompareAndSet(oldValue, oldValue + 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();
}
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(optimisticLocking.num);
service.shutdownNow();
}
}
总结: 本文代码主要为了实现原子自增,来了解JUC中原子类的实现原理 关键点是通过对volatile修饰的变量实现CAS+自旋操作. 自旋的目的: 如果没有通过CAS设置value成功, 就获取当前value值, 重新CAS设置.
另外, 其实在java并发包中提供了很多原子类, 作用是在并发编程中, 实现线程安全的原子操作 - 自增,自减, CAS设置值等. 这些内容属于原子类的使用问题, 用起来很简单, 关键是其底层原理都使用了我们今天介绍的Unsafe的CAS操作. 下面列举部分源码作为说明
Step1: AotmicInteger的自增1 –> java.util.concurrent.atomic.AtomicInteger 关键点1: 调用Unsafe的函数
public final int getAndDecrement() {
return U.getAndAddInt(this, VALUE, -1);
}
Step2: Unsafe的getAndAddInt函数调用 –> jdk.internal.misc.Unsafe
@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;
}
–> jdk.internal.misc.Unsafe
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
–> jdk.internal.misc.Unsafe
@HotSpotIntrinsicCandidate
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的修改, 也就是保证线程之间的可见性, 所以必须加上它. 至此,通过自己手写了一个自增原子操作, 另外解析了一个原子类自增源码. 原子类的使用, 非常简单就不多罗列了. 后续想到继续添加.
关于本文, 有疑问欢迎通过留言交流.
|