热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

Java并发编程学习之Unsafe类与LockSupport类源码详析

这篇文章主要给大家介绍了关于Java并发编程学习之Unsafe类与LockSupport类源码的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧

一.Unsafe类的源码分析

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是native方法,通过使用JNI的方式来访问本地C++实现库。

rt.jar 中 Unsafe 类主要函数讲解, Unsafe 类提供了硬件级别的原子操作,可以安全的直接操作内存变量,其在 JUC 源码中被广泛的使用,了解其原理为研究 JUC 源码奠定了基础。

首先我们先了解Unsafe类中主要方法的使用,如下:

  1.long objectFieldOffset(Field field)  方法:返回指定的变量在所属类的内存偏移地址,偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用unsafe获取AtomicLong中变量value在AtomicLong对象中的内存偏移,代码如下:

static {
 try {
 valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
 } catch (Exception ex) {
 throw new Error(ex);
 }

 }

  2.int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址 

  3.int arrayIndexScale(Class arrayClass)方法:获取数组中单个元素占用的字节数 

  3.boolean compareAndSwapLong(Object obj,long offset,long expect,long update)方法:比较对象obj中偏移量offset的变量的值是不是和expect相等,相等则使用update值更新,然后返回true,否则返回false。 

  4.public native long getLongVolative(Object obj,long offset)方法:获取对象obj中偏移量offset的变量对应的volative内存语义的值。 

  5.void putOrderedLong(Object obj, long offset, long value) 方法:设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。这是有延迟的 putLongVolatile 方法,并不保证值修改对其它线程立刻可见。变量只有使用 volatile 修饰并且期望被意外修改的时候使用才有用。 

  6.void park(boolean isAbsolute, long time) 方法:阻塞当前线程,其中参数 isAbsolute 等于 false 时候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,也就是相对当前时间累加 time 后当前线程就会被唤醒。 如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的时间点后会被唤醒,这里 time 是个绝对的时间,是某一个时间点换算为 ms 后的值。另外当其它线程调用了当前阻塞线程的 interrupt 方法中断了当前线程时候,当前线程也会返回,当其它线程调用了 unpark 方法并且把当前线程作为参数时候当前线程也会返回。 

  7.void unpark(Object thread)方法: 唤醒调用 park 后阻塞的线程,参数为需要唤醒的线程。 

在JDK1.8中新增加了几个方法,这里简单的列出Long类型操作的方法如下:

  8.long getAndSetLong(Object obj, long offset, long update) 方法: 获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量 volatile 语义的值为 update。使用方法如下代码:

public final long getAndSetLong(Object obj, long offset, long update)
 {
 long l;
 do
 {
 l = getLongVolatile(obj, offset);//(1)
 } while (!compareAndSwapLong(obj, offset, l, update));
 return l;
 }

从代码中可以内部代码(1)处使用了getLongVolative获取当前变量的值,然后使用CAS原子操作进行设置新值,这里使用while循环是考虑到多个线程同时调用的情况CAS失败后需要自旋重试。 

  9.long getAndAddLong(Object obj, long offset, long addValue) 方法 :获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量值为原始值 +addValue。使用方法如下代码:

public final long getAndAddLong(Object obj, long offset, long addValue)
 {
 long l;
 do
 {
 l = getLongVolatile(obj, offset);
 } while (!compareAndSwapLong(obj, offset, l, l + addValue));
 return l;
 }

类似于getAndSetLong的实现,只是这里使用CAS的时候使用了原始值+传递的增量参数addValue的值。 

那么如何使用Unsafe类呢?

  看到 Unsafe 这个类如此牛叉,是不是很想进行练习,好了,首先看如下代码所示:

package com.hjc;
import sun.misc.Unsafe;
/**
 * Created by cong on 2018/6/6.
 */
public class TestUnSafe {
 //获取Unsafe的实例(2.2.1)
 static final Unsafe unsafe = Unsafe.getUnsafe();
 //记录变量state在类TestUnSafe中的偏移值(2.2.2)
 static final long stateOffset;
 //变量(2.2.3)
 private volatile long state = 0;
 static {
 try {
  //获取state变量在类TestUnSafe中的偏移值(2.2.4)
  stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

 } catch (Exception ex) {
  System.out.println(ex.getLocalizedMessage());
  throw new Error(ex);
 }
 }

 public static void main(String[] args) {
 //创建实例,并且设置state值为1(2.2.5)
 TestUnSafe test = new TestUnSafe();
 //(2.2.6)
 Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
 System.out.println(sucess);
 }
}

代码(2.2.1)获取了Unsafe的一个实例,代码(2.2.3)创建了一个变量state初始化为0.

代码(2.2.4)使用unsafe.objectFieldOffset 获取 TestUnSafe类里面的state变量 在 TestUnSafe对象里面的内存偏移量地址并保存到stateOffset变量。

代码(2.2.6)调用创建的unsafe实例的compareAndSwapInt方法,设置test对象的state变量的值,具体意思是如果test对象内存偏移量为stateOffset的state的变量为0,则更新改值为1 

上面代码我们希望输入true,然而执行后会输出如下结果:

为什么会这样呢?必然需要进入getUnsafe代码中如看看里面做了啥:

private static final Unsafe theUnsafe = new Unsafe();
 public static Unsafe getUnsafe(){
 //(2.2.7)
 Class localClass = Reflection.getCallerClass();
 //(2.2.8)
 if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {
 throw new SecurityException("Unsafe");
 }
 return theUnsafe;
}

 //判断paramClassLoader是不是BootStrap类加载器(2.2.9)
 public static boolean isSystemDomainLoader(ClassLoader paramClassLoader){
 return paramClassLoader == null;
 }

代码(2.2.7)获取调用getUnsafe这个方法的对象的Class对象,这里是TestUnSafe.calss。

代码(2.2.8)判断是不是Bootstrap类加载器加载的localClass,这里关键要看是不是Bootstrap加载器加载了TestUnSafe.class。看过Java虚拟机的类加载机制的人,很明显看出是由于TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。

那么问题来了,为什么需要有这个判断呢?

我们知道Unsafe类是在rt.jar里面提供的,而rt.jar里面的类是使用Bootstrap类加载器加载的,而我们启动main函数所在的类是使用AppClassLoader加载的,所以在main函数里面加载Unsafe类时候鉴于双亲委派机制会委托给Bootstrap去加载Unsafe类。

如果没有代码(2.2.8)这个鉴权,那么我们应用程序就可以随意使用Unsafe做事情了,而Unsafe类可以直接操作内存,是很不安全的,所以JDK开发组特意做了这个限制,不让开发人员在正规渠道下使用Unsafe类,而是在rt.jar里面的核心类里面使用Unsafe功能。 

问题来了,如果我们真的想要实例化Unsafe类,使用Unsafe的功能,那该怎么办呢?

我们不要忘记了反射这个黑科技,使用万能的反射来获取Unsafe的实例方法,代码如下:

package com.hjc;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
 * Created by cong on 2018/6/6.
 */
public class TestUnSafe {
 static final Unsafe unsafe;
 static final long stateOffset;
 private volatile long state = 0;
 static {
 try {
  // 反射获取 Unsafe 的成员变量 theUnsafe(2.2.10)
  Field field = Unsafe.class.getDeclaredField("theUnsafe");
  // 设置为可存取(2.2.11)
  field.setAccessible(true);

  // 获取该变量的值(2.2.12)
  unsafe = (Unsafe) field.get(null);
  //获取 state 在 TestUnSafe 中的偏移量 (2.2.13)
  stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
 } catch (Exception ex) {
  System.out.println(ex.getLocalizedMessage());
  throw new Error(ex);
 }
 }

 public static void main(String[] args) {
 TestUnSafe test = new TestUnSafe();
 Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
 System.out.println(sucess);
 }
}

如果上面的代码(2.2.10    2.2.11   2.2.12)反射获取unsafe的实例,运行结果如下:

 

二.LockSupport类源码探究

JDK中的rt.jar里面的LockSupport是一个工具类,主要作用是挂起和唤醒线程,它是创建锁和其他同步类的基础。

LockSupport类与每个使用他的线程都会关联一个许可证,默认调用LockSupport 类的方法的线程是不持有许可证的,LockSupport内部使用Unsafe类实现。

这里要注意LockSupport的几个重要的函数,如下:

  1.void park() 方法: 如果调用 park() 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,否者调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。例子如下代码:

package com.hjc;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main( String[] args ) {
 System.out.println( "park start!" );
 LockSupport.park();
 System.out.println( "park stop!" );
 }
}

如上面代码所示,直接在main函数里面调用park方法,最终结果只会输出park start!  然后当前线程会被挂起,这是因为默认下调用线程是不持有许可证的。运行结果如下:

 

在看到其他线程调用 unpark(Thread thread) 方法并且当前线程作为参数时候,调用park方法被阻塞的线程会返回,另外其他线程调用了阻塞线程的interrupt()方法,设置了中断标志时候或者由于线程的虚假唤醒原因后阻塞线程也会返回,所以调用 park() 最好也是用循环条件判断方式。

需要注意的是调用park()方法被阻塞的线程被其他线程中断后阻塞线程返回时候并不会抛出InterruptedException 异常。

  2.void unpark(Thread thread) 方法 当一个线程调用了 unpark 时候,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。如果 thread 之前调用了 park() 被挂起,则调用 unpark 后,该线程会被唤醒。

如果 thread 之前没有调用 park,则调用 unPark 方法后,在调用 park() 方法,会立刻返回,上面代码修改如下:

package com.hjc;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main( String[] args ) {
 System.out.println( "park start!" );
 //使当前线程获取到许可证
 LockSupport.unpark(Thread.currentThread());
 //再次调用park
 LockSupport.park();
 System.out.println( "park stop!" );
 }
}

运行结果如下:

 

 接下来我们在看一个例子来加深对 park,unpark 的理解,代码如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main(String[] args) throws InterruptedException {
 Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
  System.out.println("子线程 park start!");
  // 调用park方法,挂起自己
  LockSupport.park();
  System.out.println("子线程 unpark!");
  }
 });

 //启动子线程
 thread.start();
 //主线程休眠1S
 Thread.sleep(1000);
 System.out.println("主线程 unpark start!");
 //调用unpark让thread线程持有许可证,然后park方法会返回
 LockSupport.unpark(thread);
 }
}

运行结果如下:

 

上面的代码首先创建了一个子线程thread,启动后子线程调用park方法,由于默认子线程没有持有许可证,会把自己挂起

主线程休眠1s 目的是主线程在调用unpark方法让子线程输出 子线程park start! 并阻塞。

主线程然后执行unpark方法,参数为子线程,目的是让子线程持有许可证,然后子线程调用的park方法就返回了。

park方法返回时候不会告诉你是因为何种原因返回,所以调用者需要根据之前是处于什么目前调用的park方法,再次检查条件是否满足,如果不满足的话,还需要再次调用park方法。

例如,线程在返回时的中断状态,根据调用前后中断状态对比就可以判断是不是因为被中断才返回的。

为了说明调用 park 方法后的线程被中断后会返回,修改上面例子代码,删除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代码如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class LockSupportTest {
 public static void main(String[] args) throws InterruptedException {
 Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
  System.out.println("子线程 park start!");
  // 调用park方法,挂起自己,只有中断才会退出循环
  while (!Thread.currentThread().isInterrupted()) {
   LockSupport.park();

  }
  System.out.println("子线程 unpark!");
  }
 });

 //启动子线程
 thread.start();
 //主线程休眠1S
 Thread.sleep(1000);
 System.out.println("主线程 unpark start!");
 //中断子线程
 thread.interrupt();
 }
}

运行结果如下:

正如上面代码,也就是只有当子线程被中断后子线程才会运行结束,如果子线程不被中断,即使你调用unPark(Thread) 子线程也不会结束。

  3.void parkNanos(long nanos)方法:和 park 类似,如果调用 park 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,不同在于如果没有拿到许可调用线程会被挂起 nanos 时间后在返回。

park 还支持三个带有blocker参数的方法,当线程因为没有持有许可证的情况下调用park  被阻塞挂起的时候,这个blocker对象会被记录到该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取该blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker设置为this,这样当内存dump排查问题时候就能知道是哪个类被阻塞了。

例子如下:

import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class TestPark {
 public void testPark(){
  LockSupport.park();//(1)
 }
 public static void main(String[] args) {
  TestPark testPark = new TestPark();
  testPark.testPark();
 }
}

运行结果如下:

可以看到运行在阻塞,那么我们要使用JDK/bin目录下的工具看一下了,如果不知道的读者,建议去先看一下JVM的监控工具。

运行后使用jstack pid 查看线程堆栈的时候,可以看到的结果如下:

然后我们进行上面的代码(1)进行修改如下:

 LockSupport.park(this);//(1)

再次运行,再用jstack pid 查看的结果如下:

可以知道,带blocker的park方法后,线程堆栈可以提供更多有关阻塞对象的信息。

那么我们接下来进行park(Object blocker) 函数的源代码查看,源码如下:

public static void park(Object blocker) {
 //获取调用线程
 Thread t = Thread.currentThread();
 //设置该线程的 blocker 变量
 setBlocker(t, blocker);
 //挂起线程
 UNSAFE.park(false, 0L);
 //线程被激活后清除 blocker 变量,因为一般都是线程阻塞时候才分析原因
 setBlocker(t, null);
}

Thread类里面有个变量volatile Object parkBlocker 用来存放park传递的blocker对象,也就是把blocker变量存放到了调用park方法的线程的成员变量里面

  4.void parkNanos(Object blocker, long nanos) 函数 相比 park(Object blocker) 多了个超时时间。

  5.void parkUntil(Object blocker, long deadline)  parkUntil源代码如下:

public static void parkUntil(Object blocker, long deadline) {
  Thread t = Thread.currentThread();
  setBlocker(t, blocker);
  //isAbsolute=true,time=deadline;表示到 deadline 时间时候后返回
  UNSAFE.park(true, deadline);
  setBlocker(t, null);
 }

可以看到是一个设置deadline,时间单位为milliseconds,是从1970到现在某一个时间点换算为毫秒后的值,这个和parkNanos(Object blocker,long nanos)区别是后者是从当前算等待nanos时间的,而前者是指定一个时间点,

比如我们需要等待到2018.06.06 日 20:34,则把这个时间点转换为从1970年到这个时间点的总毫秒数。

我们再来看一个例子,代码如下:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
/**
 * Created by cong on 2018/6/6.
 */
public class FIFOMutex {
 private final AtomicBoolean locked = new AtomicBoolean(false);
 private final Queue waiters = new ConcurrentLinkedQueue();
 public void lock() {
  boolean wasInterrupted = false;
  Thread current = Thread.currentThread();
  waiters.add(current);
  // 只有队首的线程可以获取锁(1)
  while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
   LockSupport.park(this);
   if (Thread.interrupted()) // (2)
    wasInterrupted = true;
  }

  waiters.remove();
  if (wasInterrupted) // (3)
   current.interrupt();
 }

 public void unlock() {
  locked.set(false);
  LockSupport.unpark(waiters.peek());
 } 
}

可以看到这是一个先进先出的锁,也就是只有队列首元素可以获取所,代码(1)如果当前线程不是队首或者当前锁已经被其他线程获取,则调用park方法挂起自己。

接着代码(2)做判断,如果park方法是因为被中断而返回,则忽略中断,并且重置中断标志,只做个标记,然后再次判断当前线程是不是队首元素或者当先锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。

然后代码(3)中如果标记为true 则中断该线程,这个怎么理解呢?其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要恢复下。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。


推荐阅读
  • 深入探讨:Actor模型如何解决并发与分布式计算难题
    在现代软件开发中,高并发和分布式系统的设计面临着诸多挑战。本文基于Akka最新文档,详细探讨了Actor模型如何有效地解决这些挑战,并提供了对并发和分布式计算的新视角。 ... [详细]
  • RTThread线程间通信
    线程中通信在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取& ... [详细]
  • Redis:缓存与内存数据库详解
    本文介绍了数据库的基本分类,重点探讨了关系型与非关系型数据库的区别,并详细解析了Redis作为非关系型数据库的特点、工作模式、优点及持久化机制。 ... [详细]
  • 关于进程的复习:#管道#数据的共享Managerdictlist#进程池#cpu个数1#retmap(func,iterable)#异步自带close和join#所有 ... [详细]
  • 本文详细介绍了如何对一个整数的二进制表示进行逆序操作。通过多种方法,包括直接法、查表法和分治法,帮助读者全面理解和掌握这一技术。 ... [详细]
  • 我自己做了一个网站图片的抓取,感觉速度有点慢抓取4000张图片可能得用15分钟左右的时间,我百度看用线程可以加快抓取,然后创建了5个线程抓取,但是5个线程是同步执行同样的操作一个图片就 ... [详细]
  • 在iOS开发中,多线程技术的应用非常广泛,能够高效地执行多个调度任务。本文将重点介绍GCD(Grand Central Dispatch)在多线程开发中的应用,包括其函数和队列的实现细节。 ... [详细]
  • 面试题总结_2019年全网最热门的123个Java并发面试题总结
    面试题总结_2019年全网最热门的123个Java并发面试题总结 ... [详细]
  • 本文将深入探讨 iOS 中的 Grand Central Dispatch (GCD),并介绍如何利用 GCD 进行高效多线程编程。如果你对线程的基本概念还不熟悉,建议先阅读相关基础资料。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • 1、什么是过滤器管道使用竖线(|)将两个命令隔开,竖线左边命令的输出就会作为竖线右边命令的输入。连续使用竖线表示第一个命令的输出会作为第二个命令的输入,第二个命令的输出又会作为第三个命令的输入, ... [详细]
  • JUC并发编程——线程的基本方法使用
    目录一、线程名称设置和获取二、线程的sleep()三、线程的interrupt四、join()五、yield()六、wait(),notify(),notifyAll( ... [详细]
author-avatar
810526猪肝
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有