Java并发指南14:JUC中常用的Unsafe和Locksupport
本文转自网络,侵删
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
喜欢的话麻烦点下Star哈
文章同步发于我的个人博客:
www.how2playlife.com
本文是微信公众号【Java技术江湖】的《Java并发指南》其中一篇,本文大部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何全面深入地学习Java并发技术,从Java多线程基础,再到并发编程的基础知识,从Java并发包的入门和实战,再到JUC的源码剖析,一步步地学习Java并发编程,并上手进行实战,以便让你更完整地了解整个Java并发编程知识体系,形成自己的知识框架。
为了更好地总结和检验你的学习成果,本系列文章也会提供一些对应的面试题以及参考答案。
如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。
前言
最近在看Java并发包的源码,发现了神奇的Unsafe类,仔细研究了一下,在这里跟大家分享一下。
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Oracle正在计划从Java 9中去掉Unsafe类,如果真是如此影响就太大了。
通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。其源码如下:
1 public static Unsafe getUnsafe() {
2 Class var0 = Reflection.getCallerClass();
3 if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
4 throw new SecurityException("Unsafe");
5 } else {
6 return theUnsafe;
7 }
8 }
网上也有一些办法来用主类加载器加载用户代码,比如设置bootclasspath参数。但更简单方法是利用Java反射,方法如下:
1 Field f = Unsafe.class.getDeclaredField("theUnsafe");
2 f.setAccessible(true);
3 Unsafe unsafe = (Unsafe) f.get(null);
获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:
一、内存管理。包括分配内存、释放内存等。
该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。
利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。
二、非常规的对象实例化。
allocateInstance()方法提供了另一种创建实例的途径。通常我们可以用new或者反射来实例化对象,使用allocateInstance()方法可以直接生成对象实例,且无需调用构造方法和其它初始化方法。
这在对象反序列化的时候会很有用,能够重建和设置final字段,而不需要调用构造方法。
三、操作类、对象、变量。
这部分包括了staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)等方法。
通过这些方法我们可以获取对象的指针,通过对指针进行偏移,我们不仅可以直接修改指针指向的数据(即使它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。
四、数组操作。
这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。
由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。
五、多线程同步。包括锁机制、CAS操作等。
这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。
其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。
Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。compareAndSwap方法是原子的,可以避免繁重的锁机制,提高代码效率。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。
六、挂起与恢复。
这部分包括了park、unpark等方法。
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
七、内存屏障。
这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。
loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。
Unsafe类是啥?
Java最初被设计为一种安全的受控环境。尽管如此,Java HotSpot还是包含了一个“后门”,提供了一些可以直接操控内存和线程的低层次操作。这个后门类——sun.misc.Unsafe——被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是丝毫不建议在生产环境中使用这个后门。因为这个API十分不安全、不轻便、而且不稳定。这个不安全的类提供了一个观察HotSpot JVM内部结构并且可以对其进行修改。有时它可以被用来在不适用C++调试的情况下学习虚拟机内部结构,有时也可以被拿来做性能监控和开发工具。
为什么叫Unsafe?
Java官方不推荐使用Unsafe类,因为官方认为,这个类别人很难正确使用,非正确使用会给JVM带来致命错误。而且未来Java可能封闭丢弃这个类。
1、简单介绍
LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。可以做到与join() 、wait()/notifyAll() 功能一样,使线程自由的阻塞、释放。
Java锁和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象队列同步器),就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。
补充:AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,
如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、简单原理
LockSupport方法底层都是调用Unsafe的方法实现。全名sun.misc.Unsafe,该类可以直接操控内存,被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是不建议在生产环境中使用这个类。因为这个API十分不安全、不轻便、而且不稳定。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit是相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就会将1变成0,同时park立即返回。
再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。意思就是说unpark 之后,如果permit 已经变为1,之后,再执行unpark ,permit 依旧是1。下边有例子会说到。
3、简单例子
以下边的做饭例子,正常来说,做饭 之前,要有锅、有菜才能开始做饭 。具体如下:
(1)先假设已经有了锅 ,那只需要买菜就可以做饭。如下,即注释掉了买锅的步骤:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//买锅
// Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
// t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
// LockSupport.park();
// System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来 了...");
System.out.println("开始做饭");
}
}
class BuyGuo implements Runnable{
private Object threadObj;
public BuyGuo(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("去买锅...");
LockSupport.unpark((Thread)threadObj);
}
}
class BuyCai implements Runnable{
private Object threadObj;
public BuyCai(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("买菜去...");
LockSupport.unpark((Thread)threadObj);
}
}
执行后,可出现下面的结果:
买菜去...
菜买回来了...
开始做饭
如上所述,可以达到阻塞主线程等到买完菜之后才开始做饭。这即是park()、unpark() 的用法。简单解释一下上述的步骤:
main 方法启动后,主线程 和 买菜线程 同时开始执行。
因为两者同时进行,当主线程 走到park() 时,发现permit 还为0 ,即会等待在这里。
当买菜线程执行进去后,走到unpark() 会将permit 变为1 。
主线程 park() 处发现permit 已经变成1 ,就可以继续往下执行了,同时消费掉permit ,重新变成0 。
以上permit 只是park/unpark 执行的一种逻辑开关,执行的步骤大致如此。
4、注意点及思考
(1)必须将park()与uppark() 配对使用才更高效。
如果上边也把买锅的线程放开,main 方法改为如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
即调用了两次park() 和unpark() ,发现有时候可以,有时候会使线程卡在那里,然后我又换了下顺序,如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("锅买回来了...");
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
原理没有详细去研究,不过想了想,上边两种其实并无区别,只是执行顺序有了影响,park() 和unpark() 既然是成对配合使用,通过标识permit 来控制,如果像前边那个例子那样,出现阻塞的情况原因,我分析可能是这么个原因:
当买锅的时候,通过unpark()将permit 置为1,但是还没等到外边的main方法执行第一个park() ,买菜的线程又调了一次unpark(),但是这时候permit 还是从1变成了1,等回到主线程调用park()的时候,因为还有两个park()需要执行,也就是需要两个消费permit ,因为permit 只有1个,所以,可能会剩下一个park()卡在那里了。
(2)使用park(Object blocker) 方法更能明确问题
其实park() 有个重载方法park(Object blocker) ,这俩方法效果差不多,但是有blocker的可以传递给开发人员更多的现场信息,可以查看到当前线程的阻塞对象,方便定位问题。所以java6新增加带blocker入参的系列park方法,替代原有的park方法。
5、与wait()/notifyAll() 的比较
LockSupport 的 park/unpark 方法,虽然与平时Object 中wait/notify 同样达到阻塞线程的效果。但是它们之间还是有区别的。
面向的对象主体不同。LockSupport() 操作的是线程对象,直接传入的就是Thread ,而wait() 属于具体对象,notifyAll() 也是针对所有线程进行唤醒。
wait/notify 需要获取对象的监视器,即synchronized修饰,而park/unpark 不需要获取对象的监视器。
实现的机制不同,因此两者没有交集。也就是说 LockSupport 阻塞的线程,notify/notifyAll 没法唤醒。但是 park 之后,同样可以被中断(interrupt()) !
JAVA高并发—LockSupport的学习及简单使用
1、简单介绍
LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。可以做到与join() 、wait()/notifyAll() 功能一样,使线程自由的阻塞、释放。
Java锁和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象队列同步器),就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。
补充:AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,
如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、简单原理
LockSupport方法底层都是调用Unsafe的方法实现。全名sun.misc.Unsafe,该类可以直接操控内存,被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是不建议在生产环境中使用这个类。因为这个API十分不安全、不轻便、而且不稳定。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。
permit是相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就会将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。意思就是说unpark 之后,如果permit 已经变为1,之后,再执行unpark ,permit 依旧是1。下边有例子会说到。
3、简单例子
以下边的做饭例子,正常来说,做饭 之前,要有锅、有菜才能开始做饭 。具体如下:
(1)先假设已经有了锅 ,那只需要买菜就可以做饭。如下,即注释掉了买锅的步骤:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//买锅
// Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
// t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
// LockSupport.park();
// System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来 了...");
System.out.println("开始做饭");
}
}
class BuyGuo implements Runnable{
private Object threadObj;
public BuyGuo(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("去买锅...");
LockSupport.unpark((Thread)threadObj);
}
}
class BuyCai implements Runnable{
private Object threadObj;
public BuyCai(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("买菜去...");
LockSupport.unpark((Thread)threadObj);
}
}
执行后,可出现下面的结果:
买菜去...
菜买回来了...
开始做饭
如上所述,可以达到阻塞主线程等到买完菜之后才开始做饭。这即是park()、unpark() 的用法。简单解释一下上述的步骤:
main 方法启动后,主线程 和 买菜线程 同时开始执行。
因为两者同时进行,当主线程 走到park() 时,发现permit 还为0 ,即会等待在这里。
当买菜线程执行进去后,走到unpark() 会将permit 变为1 。
主线程 park() 处发现permit 已经变成1 ,就可以继续往下执行了,同时消费掉permit ,重新变成0 。
以上permit 只是park/unpark 执行的一种逻辑开关,执行的步骤大致如此。
4、注意点及思考
(1)必须将park()与uppark() 配对使用才更高效。
如果上边也把买锅的线程放开,main 方法改为如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
即调用了两次park() 和unpark() ,发现有时候可以,有时候会使线程卡在那里,然后我又换了下顺序,如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("锅买回来了...");
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
原理没有详细去研究,不过想了想,上边两种其实并无区别,只是执行顺序有了影响,park() 和unpark() 既然是成对配合使用,通过标识permit 来控制,如果像前边那个例子那样,出现阻塞的情况原因,我分析可能是这么个原因:
当买锅的时候,通过unpark()将permit 置为1,但是还没等到外边的main方法执行第一个park() ,买菜的线程又调了一次unpark(),但是这时候permit 还是从1变成了1,等回到主线程调用park()的时候,因为还有两个park()需要执行,也就是需要两个消费permit ,因为permit 只有1个,所以,可能会剩下一个park()卡在那里了。
(2)使用park(Object blocker) 方法更能明确问题
其实park() 有个重载方法park(Object blocker) ,这俩方法效果差不多,但是有blocker的可以传递给开发人员更多的现场信息,可以查看到当前线程的阻塞对象,方便定位问题。所以java6新增加带blocker入参的系列park方法,替代原有的park方法。
5、与wait()/notifyAll() 的比较
LockSupport 的 park/unpark 方法,虽然与平时Object 中wait/notify 同样达到阻塞线程的效果。但是它们之间还是有区别的。
面向的对象主体不同。LockSupport() 操作的是线程对象,直接传入的就是Thread ,而wait() 属于具体对象,notifyAll() 也是针对所有线程进行唤醒。
wait/notify 需要获取对象的监视器,即synchronized修饰,而park/unpark 不需要获取对象的监视器。
实现的机制不同,因此两者没有交集。也就是说 LockSupport 阻塞的线程,notify/notifyAll 没法唤醒。但是 park 之后,同样可以被中断(interrupt()) !
Java并发指南14:JUC中常用的Unsafe和Locksupport的更多相关文章
- Java并发指南16:JUC中常用的Unsafe和Locksupport
原创文章,转载请注明: 转载自并发编程网 – ifeve.com 1. 什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个 ...
- Java并发编程(3) JUC中的锁
一 前言 前面已经说到JUC中的锁主要是基于AQS实现,而AQS(AQS的内部结构 .AQS的设计与实现)在前面已经简单介绍过了.今天记录下JUC包下的锁是怎么基于AQS上实现的 二 同步锁 同步锁不 ...
- Java并发指南14:Java并发容器ConcurrentSkipListMap与CopyOnWriteArrayList
原文出处http://cmsblogs.com/ 『chenssy』 到目前为止,我们在Java世界里看到了两种实现key-value的数据结构:Hash.TreeMap,这两种数据结构各自都有着优缺 ...
- Java并发指南8:AQS中的公平锁与非公平锁,Condtion
一行一行源码分析清楚 AbstractQueuedSynchronizer (二) 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer ...
- Java并发指南7:JUC的核心类AQS详解
一行一行源码分析清楚AbstractQueuedSynchronizer 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer#toc4 ...
- Java并发指南4:Java中的锁 Lock和synchronized
Java中的锁机制及Lock类 锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消 ...
- Java并发指南5:JMM中的final关键字解析
本文转载自互联网,侵删 与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问.对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个final域的 ...
- Java并发指南6:Java内存模型JMM总结
本文转载自互联网,侵删 在前面的文章中我们介绍了Java并发基础和线程安全的概念,以及JMM内存模型的介绍,包括其定义的各种规则.同时我们也介绍了volatile在JMM中的实现原理,以及Lock ...
- Java并发指南2:深入理解Java内存模型JMM
本文转载自互联网,侵删 一:JMM基础与happens-before 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实 ...
随机推荐
- c++调用c#代码
// ConsoleApplication1.cpp : 此文件包含 "main" 函数.程序执行将在此处开始并结束. // #include "pch.h" ...
- SpringBoot整合jsp技术
1.修改pom.xml文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns=&qu ...
- 【MM系列】SAP MM模块-基础配置第一篇
公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[MM系列]SAP MM模块-基础配置第一篇 ...
- MVC Html.AntiForgeryToken(); 防止跨站伪造请求(建议所有表单提交都加这个)
视图页面from表单中添加 @Html.AntiForgeryToken(); 然后每个表单提交的时候都会带__RequestVerificationToken 字段 后端控制器验证时添加 [Val ...
- hive DML操作
1.数据导入 1)向表中装载数据(load) 语法 hive> load data [local] inpath '/opt/module/datas/student.txt' [overwri ...
- [APIO2019] 奇怪装置
$solution:$ 问题其实就是求两个式子的循环节. 钦定 $t\mod B=0$且 $(t\neq 0)$,其 $t$ 为循环节. 则将 $1$ 式拆开得 $\frac{t\times (B+1 ...
- ORACLE之字符集修改(10g)
当从oracle服务器将数据导出成dmp文件后,再导入到本地的oracle数据库时,出现: IMP-00019: 由于 ORACLE 错误 12899 而拒绝行 IMP-00003: 遇到 ORACL ...
- SSH简单概念
Spring:轻量级控制反转(IoC)和面向切面(AOP)的容器框架,让对象与对象之间的关系通过配置文件来管理,减低耦合度 IoC:凡是在容器中配置过的对象才会有Spring提供的服务和功能 AOP: ...
- 浅谈GC
关于Java中的GC,简单来说就是垃圾收集器自动回收生命周期结束的对象,释放内存. 那么怎样确定对象是否存活呢? 可达性分析算法 现在主流的Java虚拟机大多使用这种可达性分析算法来判断对象是否需要进 ...
- 脚本_查找 Linux 系统中的僵尸进程
#!bin/bash#功能:查找Linux系统中的僵尸进程#作者:liusingbon#使用awk判断ps命令输出的第8列为Z时,显示该进程的 PID 和进程命令ps aux |awk '{if($8 ...