Java并发: 基于Unsafe的CAS实现无锁数据结构

在上一篇Java并发: 面临的挑战文章中说过CAS是解决原子性问题的方案之一。Unsafe提供了CAS的支持,支持实例化对象、访问私有属性、堆外内存访问、线程的启停等功能。

许多Java的并发类库都是基于Unsafe实现的,比如原子类AtomicInteger,并发数据结构ConcurrentHashMap,锁的基础组件LockSupport、AbstractQueuedSynchronizer等。Unsafe在JDK内部扮演着重要的角色。

这一篇我们学习Unsafe的使用,并用Unsafe实现一个自增ID生成器。

1. Unsafe的使用

1. 获取Unsafe

虽然Unsafe提供了getUnsafe()方法获取Unsafe对象,但是处于安全考虑,JDK限制必须是Root或者Platform Classloader加载的类才能使用getUnsafe()方法。好在我们能用反射获取Unsafe对象

private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);return (Unsafe) field.get(null);
}
2. 实例化对象

通过Unsafe能够实例化一个Class的对象,特殊的是它仅分配内存,不会任何初始化,不调用构造函数。我们看个例子,假设我们有一个Data类,然后使用Unsafe实例化。

public static class Data {private int value = 1;public Data() {value = 2;}public String toString() {return "value:" + value;}
}
// 实例化代码
Data data = (Data) unsafe.allocateInstance(Data.class);
System.out.println(data);

我们看一下输出,实例化后value依然是0,可以确定的是value=1、value=2都没有被执行。

3. 访问私有属性

通过Unsafe访问私有属性,可以通过3个维度

  1. 操作,读: get,写put
  2. 类型,可选值包括Byte、Short、Int、Long、Float、Double、Char、Boolean、Object
  3. 是否包含Volatile语义

假设我们要“读Byte类型的字段,不采用Volatile语义",选择方法getByte;如果要"写Int类型字段,用Volatile语义",选择方法putIntVolatile。下面是一个简单的示例

Field valueField = Data.class.getDeclaredField("value");
long valueOffset = unsafe.objectFieldOffset(valueField);int value = unsafe.getInt(data, valueOffset);
unsafe.putIntVolatile(data, valueOffset, 3);

这里值得专门讲一下的是,使用Volatile语义的方法,比如putIntVolatile方法,和要读写的字段是否定义为volatile是没有关系的。即使字段定义是非volatile的,使用putIntVolatile修改字段,这个写依然符合volatile写的语义,即会将数据刷新到主内存中,但是因为字段是非volatile的,直接通过字段读不保证会主内从读,因此不保证可见性,改用getIntVolatile读,这时后是保证可见的。组合字段定义和调用的方法,支持volatile语义的规则如下图所示

4. 堆外内从访问

正常的对象都是在堆上分配的,会要频繁的经历GC,而且内从十分有限。Unsafe提供了机制直接在堆外申请一块内存,这些JVM和GC是不感知的,需要自己管理生命周期。对于某些需要常驻内存的场景,通过使用堆外内存,能大大的提高效率。我们来看个例子,假设我们有个OffHeapStudent类,保存学生的名字(String)和年龄(short),我们可以这样定义。Unsafe.allocateMemory能够分配一段内存,之后可以通过这个分配的内存的地址,使用类似访问私有属性的方法来读取和写入内存。

public static class OffHeapStudent {private long address;private Unsafe unsafe;private int length;public OffHeapStudent(Unsafe unsafe, String name, short age) {this.unsafe = unsafe;char[] cs = name.toCharArray();length = cs.length * 2 + 2;address = unsafe.allocateMemory(length);for (int i = 0; i < cs.length; i++) {unsafe.putChar(address + i * 2, cs[i]);}unsafe.putShort(address + cs.length * 2, age);}public String getName() {char[] cs = new char[(length - 2) / 2];for (int i = 0; i < cs.length; i++) {cs[i] = unsafe.getChar(address + i * 2);}return new String(cs);}public short getAge() {return unsafe.getShort(address + length - 2);}}

通过这段代码能测试我们的OffHeapStudent是否正常工作,看控制台输出,我们能正确的访问的name和age字段,说明我们的类正常工作了。

Unsafe unsafe = getUnsafe();
OffHeapStudent student = new OffHeapStudent(unsafe, "randy", (short) 20);
System.out.println(student.getName());
System.out.println(student.getAge());
5. CAS

Unsafe类里提供了大量的方法实现CAS,除了基础的compareAndSwapInt、compareAndSwapLong、compareAndSwapObject,还有大量的getAndAddXxx方法。我们拿之前Data类来做一个实例

public static class Data {private int value = 1;public Data() {value = 2;}public String toString() {return "value:" + value;}
}

我们可以这样使用Unsafe,第二次操作是成功的。

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {Unsafe unsafe = getUnsafe();Field valueField = Data.class.getDeclaredField("value");long valueOffset = unsafe.objectFieldOffset(valueField);Data data = new Data();boolean success = unsafe.compareAndSwapInt(data, valueOffset, 1, 3);System.out.println("success: " + success + " ,value:" + data.value);success = unsafe.compareAndSwapInt(data, valueOffset, 2, 3);System.out.println("success: " + success + " ,value:" + data.value);
}
6. Park和UnPark

通过Unsafe的park和unpark方法,我们能将线程挂起和恢复运行。这两个方法是LockSupport实现的基础,而LockSupport是Java中很多锁和同步组件的实现基础。这里我们实现一个基本示例,有个Worker线程,运行时检测时间(timeNow)是否到10点了,如果没到10点,将当前线程挂起,等待通知后在重新检测。而在另外一个线程里,我们修改时间,并且在timeNow时间到了之后,通过unpark方法恢复Worker线程的执行。Worker线程代码如下

public static class Worker implements Runnable {public void run() {while (timeNow < 10) {System.out.println("NotTimeYet, ParkThread");getUnsafe().park(false, 0);}System.out.println("It's time to work.");}
}

在main方法上,我们来启动线程,并unpark恢复线程运行

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Worker());t.start();TimeUnit.SECONDS.sleep(30);timeNow = 10;System.out.println("TimeIsUP, UnParkThread");getUnsafe().unpark(t);t.join();
}

通过控制台输出,我们能确定Unsafe.park()确实将线程挂起了,而在main线程执行unpark后恢复执行

NotTimeYet, ParkThread
TimeIsUP, UnParkThread
It's time to work.

2. 案例: 基于CAS的自增ID生成器

1. 定义接口

为了方便测试和对比,我们事先定义了ID接口,里边只有一个方法incrementAndGet: 自增并返回int值。

public interface ID {public int incrementAndGet();}
2. 对照实现

我们提供两种标准实现,CrashIntegerID: 不是线程安全的,导致中间会有ID重复;SyncIntegerID: 使用锁同步,用来作为性能比较基准。

public class CrashIntegerID implements ID{private int id;public CrashIntegerID(int start) {this.id = start;}public int incrementAndGet() {return id++;}
}
public class SyncIntegerID implements ID{private int id;public SyncIntegerID(int start) {this.id = start;}public synchronized int incrementAndGet() {return id++;}
}
3. 测试方法

提供了一个模板方法,接受ID接口的实现类,使用50个线程,通过覆写afterExecute打印执行耗时

private static void testInMultiThread(ID id) throws ExecutionException, InterruptedException {long start = System.currentTimeMillis();ExecutorService es = new ThreadPoolExecutor(50, 50, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()) {protected void afterExecute(Runnable r, Throwable t) {long now = System.currentTimeMillis();System.out.println("time cost: " + (now - start));}};for (int i = 0; i < 10_0000; i++) {es.submit(() -> {int v = id.incrementAndGet();System.out.println(v);});}es.shutdown();
}
4. 检查方法

控制台打印了所有生成的id,通过检查打印内容,我们能确定id是否有重复,比如用CrashIntegerID生成10w个id的时候,我们发现就已经有多个重复值。

randy@Randy:~$ cat num | egrep -v '^$' | sort -n | uniq -d
2643
5249
36473
53039
91835
5. CAS实现

使用Unsafe提供的getAndAdd方法对自增字段实现CAS的自增

public class CASIntegerID implements ID {private int id;private final Unsafe UNSAFE;private final long idOffset;{try {UNSAFE = getUnsafe();idOffset = UNSAFE.objectFieldOffset(CASIntegerID.class.getDeclaredField("id"));} catch (Exception e) {throw new RuntimeException(e);}}public CASIntegerID(int id) {this.id = id;}@Overridepublic int incrementAndGet() {return UNSAFE.getAndAddInt(this, idOffset, 1);}private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);return (Unsafe) field.get(null);}
}

6. 性能对比

我们分别用SyncIntegerID、CASIntegerID生成100w个ID,看一下执行耗时的差异。因为afterExecute每个任务都打印System.out可能会耗时比例,实际上差异应该会更大。

SyncIntegerID

CASIntegerID

10w

2932ms

2698ms

100w

22993ms

18429ms

A. 参考资料

  1. Guide to Unsafe,Guide to sun.misc.Unsafe | Baeldung

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/15187.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多线程(C++11)

多线程&#xff08;C&#xff09; 文章目录 多线程&#xff08;C&#xff09;前言一、std::thread类1.线程的创建1.1构造函数1.2代码演示 2.公共成员函数2.1 get_id()2.2 join()2.3 detach()2.4 joinable()2.5 operator 3.静态函数4.类的成员函数作为子线程的任务函数 二、call…

【Linux学习】深入探索进程等待与进程退出码和退出信号

文章目录 退出码return退出 进程的等待进程等待的方法 退出码 main函数的返回值&#xff1a;进程的退出码。 一般为0表示成功&#xff0c;非0表示失败。 每一个非0退出码都表示一个失败的原因&#xff1b; echo $&#xff1f;命令 作用&#xff1a;查看进程退出码。&#xf…

I.MX6ULL Linux C语言开发环境搭建(点灯实验)

系列文章目录 I.MX6ULL Linux C语言开发 I.MX6ULL Linux C语言开发 系列文章目录一、前言二、硬件原理分析三、构建步骤一、 C语言运行环境构建二、软件编写三、链接脚本 四、实验程序编写五、编译下载验证 一、前言 汇编语言编写 LED 灯实验&#xff0c;但是实际开发过程中汇…

Go语言的内存泄漏如何检测和避免?

文章目录 Go语言内存泄漏的检测与避免一、内存泄漏的检测1. 使用性能分析工具2. 使用内存泄漏检测工具3. 代码审查与测试 二、内存泄漏的避免1. 使用defer关键字2. 使用垃圾回收机制3. 避免循环引用4. 使用缓冲池 Go语言内存泄漏的检测与避免 在Go语言开发中&#xff0c;内存泄…

【已解决】C#设置Halcon显示区域Region的颜色

前言 在开发过程中&#xff0c;突然发现我需要显示的筛选区域的颜色是白色的&#xff0c;如下图示&#xff0c;这对我们来说不明显会导致我的二值化筛选的时候存在误差&#xff0c;因此我们需要更换成红色显示这样的话就可以更加的明显&#xff0c;二值化筛选更加的准确。 解…

java: 无法访问org.springframework.ldap.core.LdapTemplate

完整错误&#xff1a; java: 无法访问org.springframework.ldap.core.LdapTemplate错误的类文件: /E:/apache-maven-3.6.3/repository/org/springframework/ldap/spring-ldap-core/3.2.3/spring-ldap-core-3.2.3.jar!/org/springframework/ldap/core/LdapTemplate.class类文件具…

《2024年中国机器人行业投融资报告》| 附下载

近年来&#xff0c;国内机器人行业取得了显著的技术进步&#xff0c;包括人工智能、感知技术、自主导航等技术方面的突破&#xff0c;使得机器人能够更好地适应复杂环境和任务需求&#xff0c;带动了机器人行业加快发展。 当然&#xff0c;技术的进步是外在驱动因素&#xff0…

探索集合python(Set)的神秘面纱:它与字典有何不同?

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、集合&#xff08;Set&#xff09;与字典&#xff08;Dictionary&#xff09;的初识 1. …

L2-038 病毒溯源

详解代码 #include <iostream> #include <cstring> #include <algorithm>using namespace std;const int N 10010,M10010;int n; int h[N], e[M], ne[M], idx;//邻接表,h表示顶点&#xff0c;e表示当前边的终点&#xff0c;ne表示下一条边&#xff0c;idx当…

海外动态IP代理如何提高效率?

动态住宅IP代理之所以能够有效提升数据爬取的效率和准确性&#xff0c;主要归功于其提供的IP地址具有高度的匿名性和真实性。这些IP地址来自于真实的用户网络&#xff0c;因此相比于数据中心IP&#xff0c;它们更不容易被网站的安全系统标识为爬虫。此外&#xff0c;由于IP地址…

【vue-1】vue入门—创建一个vue应用

最近在闲暇时间想学习一下前端框架vue&#xff0c;主要参考以下两个学习资料。 官网 快速上手 | Vue.js b站学习视频 2.创建一个Vue3应用_哔哩哔哩_bilibili 一、创建一个vue3应用 <!DOCTYPE html> <html lang"en"> <head><meta charset&q…

NodeJS安装并生成Vue脚手架(保姆级)

文章目录 NodeJS下载配置环境变量Vue脚手架生成Vue脚手架创建项目Vue项目绑定git 更多相关内容可查看 NodeJS下载 下载地址&#xff1a;https://nodejs.org/en 下载的速度应该很快&#xff0c;下载完可以无脑安装&#xff0c;以下记得勾选即可 注意要记住自己的安装路径&…

【Linux】简单模拟C语言文件标准库FILE

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…

R可视化:可发表的Y轴截断图

Y轴截断图by ggprism Y轴截断图by ggprism 介绍 ggplot2绘制Y轴截断图by ggprism加载R包 knitr::opts_chunk$set(message = FALSE, warning = FALSE)library(tidyverse) library(ggprism) library(patchwork)rm(list = ls()) options(stringsAsFactors = F) options(future.…

Go语言的中间件(middleware)是如何实现的?

文章目录 Go语言的中间件&#xff08;Middleware&#xff09;是如何实现的&#xff1f;中间件的工作原理中间件的实现步骤示例代码总结 Go语言的中间件&#xff08;Middleware&#xff09;是如何实现的&#xff1f; 在Go语言中&#xff0c;中间件&#xff08;Middleware&#…

springboot实现多开发环境匹配置(超级简洁没废话)

首先logbok-spring.xml里面的内容 <?xml version"1.0" encoding"UTF-8"?> <configuration><!-- 开发、测试环境 --><springProfile name"dev,test"><include resource"org/springframework/boot/logging/log…

Java并发面试题,多线程通关秘籍

【知识点记录】- 不能不知道的知识点 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学习心得&#xf…

算法设计与分析

一、分治法 二、回溯法 三、贪心法 四、动态规划法 分治法一分而治之 对于一个规模为n的问题&#xff0c;若该问题可以容易地解决&#xff08;比如说规模n较小&#xff09;则直接解决&#xff0c;否则将其分解为k个规模较小的子问题&#xff0c;这些子问题互相独立且与原问题形…

【Linux】关于获取进程退出状态中的core dump标志补充

通过 wait/waitpid 可以获取子进程的退出状态, 从而判断其退出结果. 记录退出状态的 int 变量 status 的使用情况如下图所示: 如果是收到信号终止的话, 低 7 位为收到的终止信号, 而低第 8 位为 core dump 标志, core dump 标志有什么用呢? core dump 标志只存 0/1, 表示是否…

printf 模仿slf4j 的log.xxx效果

printf 模仿slf4j 的log.xxx效果 简介期待的效果颜色遇到的问题实际的效果代码实现使用效果图 简介 突然想玩一玩&#xff0c;能不能用printf实现slf4j里 的log.xxx 的效果。 性能是完全不考虑的&#xff0c;只要功能可用就好。 期待的效果 类似:info("这是第{}条日志…