原文链接:查看原文
感谢公众号“ 路人甲Java”的分享,如有冒犯,请联系删除,快去关注他吧
本文主要内容:
- Unsafe基本介绍
- 获取Unsafe实例
- Unsafe中的CAS操作
- Unsafe中原子操作相关方法介绍
- Unsafe中线程调度相关方法介绍
- park和unpark示例
- Unsafe锁示例
- Unsafe中对volatile的支持
基本介绍
java高并发中主要涉及到的类位于 java.util.concurrent 包中,简称juc,juc中大部分类都是依赖于Unsafe来实现的,主要用到了Unsafe中的CAS、线程挂起、线程恢复等相关功能。所以如果打算深入了解JUC原理的,必须先了解一下Unsafe类。
先看一张图:
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确的使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
从Unsafe功能图上来看,Unsafe提供的API大致可分为 内存操作、CAS 、Class相关、对象操作 、 线程调度 、系统信息获取 、 内存屏障 、 数组操作 等几类,本文主要介绍3个常用的操作:CAS、线程调度和对象操作。
看一下Unsafe的部分源码:
public final class Unsafe{
//单例对象
public static final Unsafe theUnsafe;
private Unsafe(){
}
@CallerSensitive
public static Unsafe getUnsafe(){
Class var0 = Reflection.getCallerClass();
//仅在引导器类加载器`BootStrapClassLoader` 加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader)){
throw new SecurityException("Unsafe");
} else{
return theUnsafe;
}
}
}
从代码中可以看出,Unsafe类为单例实现,提供静态方法getUnsafe获取Unsafe实例,内部会判断当前调用者是否是由系统类加载器加载的,如果不是系统类加载器加载的,会抛出 SecurityException 异常。
那我们想使用这个类,如何获取呢?
可以把这个类放在jdk的lib目录下,那么启动时就会自动加载,这种方式不是很好。
我们学过反射,通过反射可以获取到 Unsafe 中的 theUnSafe 字段的值,这样可以获取到Unsafe对象的实例。
通过反射获取Unsafe实例
代码如下:
package aboutThread.Concurrent.Day22;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class Demo1 {
static Unsafe unsafe;
static{
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args){
System.out.println(unsafe);
}
}
输出:
sun.misc.Unsafe@5a07e868
Unsafe中的CAS操作
看一下Unsafe中的CAS相关方法定义:
/**
* CAS 操作
*
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是CAS? 即比较并替换,实现并发算法时常用的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作时,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不作任何操作,多个线程同时执行cas操作,只有一个成功。 我们知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致的问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的,其实在这一点上还是有排他锁的,只是比起用synchronized,这里的排他时间要短的多,所以在多线程下性能会比较好。
说一下offset,offset为字段的偏移量,每个对象有个地址,offset是字段相对于对象地址的偏移量,对象地址记为baseAddress,字段偏移量记为offset,那么字段对应的实际地址就是baseAddress+offset ,所以cas通过对象、偏移量就可以去操作字段对应的值了。
CAS在java.util.concurrent.atomic相关类、Java AQS、JUC中并发集合等实现上有非常广泛的应用,我们看一下 java.util.concurrent.atomic.AtomicInteger 类,这个类可以在多线程环境中对int类型的数据执行高效的原子修改操作,并保证数据的正确性,看一下此类中用到的Unsafe cas的地方:
JUC中其他用到CAS的地方就不列举了,看源码去喽!
Unsafe中原子操作相关方法介绍
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
@ForceInline
public final int getAndAddInt(Object o, long offset, int delta) {
return theInternalUnsafe.getAndAddInt(o, offset, delta);
}
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
@ForceInline
public final long getAndAddLong(Object o, long offset, long delta) {
return theInternalUnsafe.getAndAddLong(o, offset, delta);
}
/**
* Atomically exchanges the given value with the current value of
* a field or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param newValue new value
* @return the previous value
* @since 1.8
*/
@ForceInline
public final int getAndSetInt(Object o, long offset, int newValue) {
return theInternalUnsafe.getAndSetInt(o, offset, newValue);
}
/**
* Atomically exchanges the given value with the current value of
* a field or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param newValue new value
* @return the previous value
* @since 1.8
*/
@ForceInline
public final long getAndSetLong(Object o, long offset, long newValue) {
return theInternalUnsafe.getAndSetLong(o, offset, newValue);
}
/**
* Atomically exchanges the given reference value with the current
* reference value of a field or array element within the given
* object {@code o} at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param newValue new value
* @return the previous value
* @since 1.8
*/
@ForceInline
public final Object getAndSetObject(Object o, long offset, Object newValue) {
return theInternalUnsafe.getAndSetReference(o, offset, newValue);
}
看一下上面的方法,内部通过自旋的CAS操作实现的,这些方法都可以保证操作的数据在多线程环境下的原子性,正确性。
来个示例,我们还是来实现一个网站计数功能,同时有100个人发起对网站的请求,每个人发起10次请求,每次请求算一次,最终结果是1000次,代码如下:
package aboutThread.Concurrent.Day22;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import sun.misc.Unsafe;
/**
* @author Aran
*/
public class Demo2 {
static Unsafe unsafe;
//用来记录网站访问量,每次访问+1
static int count;
//count在Demo.class对象中的地址偏移量
static long countOffset;
static {
try {
//获取Unsafe对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field countField = Demo2.class.getDeclaredField("count");
//获取count字段在Demo2中的内存地址的偏移量
countOffset = unsafe.staticFieldOffset(countField);
} catch (Exception e) {
e.printStackTrace();
}
}
//模拟访问一次
public static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//对count原子加1
unsafe.getAndAddInt(Demo2.class, countOffset, 1);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
}
}
输出:
main,耗时:90,count=1000
代码中我们在静态块中通过反射获取到了Unsafe类的实例,然后获取Demo2中count字段内存地址偏移量 countOffset ,main方法中模拟了100个人,每人发10次请求,等到所有请求完毕之后,输出count结果。
代码中用到 CountDownLath ,通过countDownLatch.await()让主线程等待,等待100个子线程都执行完毕后,主线程在进行运行。CountDownLatch 的使用可以参考第16天的文章
Unsafe中线程调度的相关方法
这部分,包括线程挂起,恢复,锁机制等方法
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程,isAbsolute:是否是绝对时间,如果为true,time是一个绝对时间,如果为false,time是一个相对时间,time表示纳秒
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
调用park 后,线程将被阻塞到,直到 unpark 调用或者超时,如果之前调用过umpark ,不会进行阻塞,即 park 和 unpark 不区分先后顺序,后面三个方法已过期,不建议使用。
park和unpark实例
package aboutThread.Concurrent.Day22;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import sun.misc.Unsafe;
public class Demo3 {
static Unsafe unsafe;
static{
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 调用park和unpark,模拟线程的挂起和唤醒
*
* @throws InterruptedException
*/
public static void m1() throws InterruptedException{
Thread thread = new Thread(() ->{
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ":start!");
unsafe.park(false, 0);
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ":end!");
});
thread.setName("t1");
thread.start();
TimeUnit.SECONDS.sleep(5);
unsafe.unpark(thread);
}
/**
* 阻塞指定的时间
*/
public static void m2(){
Thread thread = new Thread(() ->{
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ":start!");
//线程挂起3秒
unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ":end!");
});
thread.setName("t2");
thread.start();
}
public static void main(String[] args) throws InterruptedException{
m1();
m2();
}
}
输出:
1591759231159,t1:start!
1591759236163,t1:end!
1591759236164,t2:start!
1591759239169,t2:end!
m1()中t1调用park方法,park方法会将 当前线程阻塞,被阻塞5秒之后,被主线程调用unpark方法给唤醒了,unpark方法参数表示需要唤醒的线程。
线程中相当于有个许可,许可默认为0,调用park的时候,发现是0会阻塞当前线程,调用unpark之后,许可会被置为1,并会唤醒当前线程。如果在park之前先调用了unpark方法,执行park方法的时候,不会阻塞。park方法被唤醒之后,许可又会被置为0,多次调用unpark的效果是一样的,许可还是1。
juc中 LockSupport 类是通过unpark和park方法实现的,需要了解的同学,可以移步:第14天的文章
Unsafe 锁示例
package aboutThread.Concurrent.Day22;
import java.lang.reflect.Field;
import java.util.Currency;
import java.util.concurrent.CountDownLatch;
import sun.misc.Unsafe;
public class Demo4 {
static Unsafe unsafe;
//用来记录网站的访问量,每次访问+1
static int count;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模拟访问一次
*/
@Deprecated
public static void request(){
unsafe.monitorEnter(Demo4.class);
try {
count++;
} catch (Exception e) {
unsafe.monitorExit(Demo4.class);
}
}
public static void main(String[] args) throws InterruptedException{
long startTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() ->{
try {
for(int j = 0; j < 10;j++){
request();
}
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - startTime) + ",count="+ count);
}
}
输出:
我的环境没有运行起来,找不到这个方法,难道是我的环境有问题
main,耗时:64,count=1000
注意:
- monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了
- monitorEnter、monitorExit必须成对出现,出现的次数必须一致,也就是说锁了n次,也必须释放n次,否则会造成死锁
Unsafe中保证变量的可见性
关于变量可见性需要先了解Java内存模型,可以移步到:
java中操作内存分为主内存和工作内存,共享数据在主内存中,线程如果需要操作主内存的数据,需要先将主内存的数据复制到线程独有的工作内存中,操作完成之后再将其刷新到主内存中。如果线程A要想看到线程B修改后的数据,需要满足:线程B修改完成后,需要将数据偶从自己的工作内存中刷新到主内存中,并且A需要去主内存中读取数据。
被关键字volatile修饰的数据:
- 如果一个变量被volatile修饰,读取这个变量的时候,会强制从主内存中读取,然后将其复制到当前线程的工作内存中使用
- 给volatile修饰的变量赋值的时候,会强制将其赋值的结果从工作内存中刷新到主内存中
上面两点语义保证了被volatile修饰的数据在多线程中的可见性。
Unsafe中提供了volatile语义一样的功能的方法,如下:
//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
putIntVolatile方法,2个参数:
o:表示需要操作的对象
offset:表示操作对象中的某个字段地址偏移量
x:将offset对应的字段的值修改为x,并且立即刷新到主内存中
调用这个方法,会强制将工作内存中修改的数据刷新到主内存中
getIntVolatile方法,2个参数:
o:表示需要操作的对象
offset:表示操作对象中的某个字段地址偏移量
每次调用这个方法都会强制从主内存中读取值,将其复制到工作内存中使用
其他的还有几个putXXXVolatile、getXXXVolatile方法和上面的类似。
这是java并发学习的第22天,加油!