java架构之路(多线程)原子操作,Atomic与Unsafe魔术类
这次不讲原理了,主要是一些应用方面的知识,和上几次的JUC并发编程的知识点更容易理解.
上次主要说了Semaphore信号量的使用,就是一个票据的使用,我们举例了看3D电影拿3D眼镜的例子,还说了内部的抢3D眼镜,和后续排队的源码解析,还有CountDownLatch的使用,我们是用王者农药来举例的,CyclicBarrier栅栏的使用和CountDownLatch几乎是一致的,Executors用的很少我只是简单的写了一个小示例。上次遗漏了一个CountDownLatch和CyclicBarrier的区别。
CountDownLatch和CyclicBarrier的区别:
区别的根本在于有无主线程参与,这样就很容易区别了,CountDownLatch有主线程,CyclicBarrier没有主线程,我们来举两个例子,CountDownLatch主线程是游戏程序,而我们开启的10个线程是玩家加载程序,我们的游戏主程序会等待10个玩家加载完成,线程可能结束,然后主程序游戏程序继续运行。CyclicBarrier没有主线程,但是具有重复性,再举一个例子,年会了,公司团建活动,三人跨栅栏,要求是必须三人全部跨过栅栏以后才可以继续跨下一个栅栏。
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。
本次新知识
什么是原子操作?
原子(atom)本意是“不能被进一步分割的小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。就像是我们的mysql里面的提到的ACID,原子性,也是不可分割的操作,最小的单位。
我们以前说的MESI,说到了缓存行,也是上锁的最小单位,原子变更就不做过多解释了,就是把一个变量的值改为另外一个值。比较与交换我们在Semaphore源码里也接触过了,也就是CAS操作需要输入两个数值,一个旧值,一个新值,在将要变更为新值之前,会比较旧值是否已经改变,如果改变了修改失败,如果没有改变,修改成功。
Atomic的使用
在Atomic包内一共有12个类,四种原子更新方式,原子更新基本类型,原子更新数组,原子更新字段,Atomic包里的类基本都是基于Unsafe实现的包装类。

基本类型:AtomicInteger,AtomicBoolean,AtomicLong。
引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedReference、AtomicMarkableReference。
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。
属性原子修改器:AtomicLongFieldUpdater、AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater。
来一个简单的实例,就是开启10个线程然后做一个自加的操作,还是很好理解的。
public class AtomicIntegerTest {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i<10; i++){
new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("自加10次数值:--->"+atomicInteger.get());
}
}
ABA问题,ABA这样更能好理解一些,一眼就可以看出来A已经不是原来的A了,虽然值一样,但是里面的属性变成了红色的,先来看一段代码。
package com.xiaocai.main;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread main = new Thread(new Runnable() {
@Override
public void run() {
int a = atomicInteger.get();
System.out.println("操作线程"+Thread.currentThread().getName()+",修改前操作数值:"+a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+",Cas修改后操作数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
}
},"主线程");
Thread other = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();// 1+1 = 2;
System.out.println("操作线程"+Thread.currentThread().getName()+",自加后值:"+atomicInteger.get());
atomicInteger.decrementAndGet();// atomic-1 = 2-1;
System.out.println("操作线程"+Thread.currentThread().getName()+",自减后值:"+atomicInteger.get());
}
},"干扰线程");
main.start();
other.start();
}
}
我们可以看到主线程设置一个初始值为1,然后进行等待,干扰线程将1修改为2,又将2修改回1,然后主线程继续操作1修改为2,这一系列的动作,主线程并没有感知到1已经不是原来的1了。

这样的操作其实是很危险的,我们假象,小王是银行的职员,他可以操作每个账户的金额(假设啊,具体能不能我也不知道),他将撕葱的账户转走了1000万用于炒股,股市大涨,小王赚了2000万,还了1千万,自己还剩下2千万,过几天撕葱来查看自己账户钱并没有少,但是钱已经不是那个钱了,有人动过的。所以ABA问题我们还是要想办法来处理的。我们每次转账汇款的操作都是有一个流水号(回执单)的,也就是每次我们加一个版本号码就可以了,我们来改一下代码。

public class AtomicIntegerTest {
static AtomicStampedReference atomicInteger = new AtomicStampedReference<>(1,0);
public static void main(String[] args) {
Thread main = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicInteger.getStamp(); //获取当前标识别
System.out.println("操作线程"+Thread.currentThread().getName()+"修改前的版本号为:"+stamp+",修改前操作数值:"+atomicInteger.getReference());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+",Cas修改后操作数值:"+atomicInteger.getReference());
}else{
System.out.println("CAS修改失败,当前版本为:"+atomicInteger.getStamp());
}
}
},"主线程");
Thread other = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicInteger.getStamp();
atomicInteger.compareAndSet(1,2,atomicInteger.getStamp(),atomicInteger.getStamp()+1);
System.out.println("操作线程"+Thread.currentThread().getName()+",版本号为:"+stamp+",修改后的版本号为:"+atomicInteger.getStamp()+",自加后值:"+atomicInteger.getReference());
int newStamp = atomicInteger.getStamp();
atomicInteger.compareAndSet(2,1,atomicInteger.getStamp(),atomicInteger.getStamp()+1);
System.out.println("操作线程"+Thread.currentThread().getName()+",版本号为:"+newStamp+",修改后的版本号为:"+atomicInteger.getStamp()+",自减后值:"+atomicInteger.getReference());
}
},"干扰线程");
main.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
other.start();
}
}
我们先初始一个主线程,并且设置版本号为0。然后干扰线程进行修改,每次修改时版本号加一,干扰线程结束,而主线程想继续修改时,发现版本不匹配,修改失败。

其余Atomic的类使用都是大同小异的,可以自行尝试一遍。
Unsafe魔术类的使用
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的 方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增 强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了 类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。 在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语 言变得不再“安全”,因此对Unsafe的使用一定要慎重。
在过去的几篇博客里也说到了Unsafe这个类,我们需要通过反射来使用它,比如读写屏障、加锁解锁,线程的挂起操作等等。

如何获取Unsafe实例?
1、从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把 调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被 引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。 java Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径。
2、通过反射获取单例对象theUnsafe。
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
总结:
这次博客完全没有代码的解析阅读,都是一些简单的使用,我们开始时候说到了什么是原子操作,接下来我们说了Atomic类的基本使用,再就是什么是ABA问题,如何用Atomic来解决ABA问题,再就是我们的魔术类Unsafe类,越过虚拟机直接来操作我们的系统的一些操作(不是超级熟练别玩这个,玩坏了不好修复)。希望对大家在工作面试中能有一些帮助。

最进弄了一个公众号,小菜技术,欢迎大家的加入

java架构之路(多线程)原子操作,Atomic与Unsafe魔术类的更多相关文章
- [转帖]java架构之路-(面试篇)JVM虚拟机面试大全
java架构之路-(面试篇)JVM虚拟机面试大全 https://www.cnblogs.com/cxiaocai/p/11634918.html 下文连接比较多啊,都是我过整理的博客,很多答案都 ...
- java架构之路(多线程)synchronized详解以及锁的膨胀升级过程
上几次博客,我们把volatile基本都说完了,剩下的还有我们的synchronized,还有我们的AQS,这次博客我来说一下synchronized的使用和原理. synchronized是jvm内 ...
- java架构之路(多线程)JMM和volatile关键字(二)
貌似两个多月没写博客,不知道年前这段时间都去忙了什么. 好久以前写过一次和volatile相关的博客,感觉没写的那么深入吧,这次我们继续说我们的volatile关键字. 复习: 先来简单的复习一遍以前 ...
- java架构之路(多线程)大厂方式手写单例模式
上期回顾: 上次博客我们说了我们的volatile关键字,我们知道volatile可以保证我们变量被修改马上刷回主存,并且可以有效的防止指令重排序,思想就是加了我们的内存屏障,再后面的多线程博客里还有 ...
- java架构之路(多线程)JMM和volatile关键字
说到JMM大家一定很陌生,被我们所熟知的一定是jvm虚拟机,而我们今天讲的JMM和JVM虚拟机没有半毛钱关系,千万不要把JMM的任何事情联想到JVM,把JMM当做一个完全新的事物去理解和认识. 我们先 ...
- java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读
说完了我们的synchronized,这次我们来说说我们的显示锁ReetrantLock. 上期回顾: 上次博客我们主要说了锁的分类,synchronized的使用,和synchronized隐式锁的 ...
- java架构之路(多线程)JUC并发编程之Semaphore信号量、CountDownLatch、CyclicBarrier栅栏、Executors线程池
上期回顾: 上次博客我们主要说了我们juc并发包下面的ReetrantLock的一些简单使用和底层的原理,是如何实现公平锁.非公平锁的.内部的双向链表到底是什么意思,prev和next到底是什么,为什 ...
- java架构之路-(Redis专题)redis面试助力满分+
1.Redis支持的数据类型? 答:五种,在第一节redis相关的博客我就说过,String,Hash,List,Set,zSet,也就是我们的字符串,哈希,列表,集合,有序集合五种.结构图如下. 2 ...
- java学习之路--多线程实现的方法
1 继承Thread类 继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Th ...
随机推荐
- 阿里云“网红"运维工程师白金:做一个平凡的圆梦人
他是阿里云的一位 P8 运维专家,却很有野心得给自己取花名“辟拾(P10)”:他没有华丽的履历,仅凭着 26 年的热爱与坚持,一步一个脚印踏出了属于自己的技术逆袭之路:他爱好清奇,练就了能在 20 秒 ...
- Visioi形状相关应用
选择手柄为白点 按住shift的同时移动白点更为灵活 黄色的点就是控制手柄(只有一维图形有) 当调整形状出现绿色边的时候说明:这个时候这个形状的边等于了某个形状的长 铅笔工具可以移动控制点来更形状 ...
- k8s集群———单master节点2node节点
#部署node节点 ,将kubelet-bootstrap用户绑定到系统集群角色中(颁发证书的最小权限) kubectl create clusterrolebinding kubelet-boots ...
- 嗨,让我带你逐行剖析Vue.js源码
本项目受到了阮一峰老师的肯定,已刊登在阮一峰老师微信公众号的科技爱好者周刊第87期,同时也被多个微博大V转发,短短一个月时间内在github上star数量就已经突破2k! Hello,大家好,我最近在 ...
- 【题解】AcWing 110. 防晒(普及题)
[题解]AcWing 110. 防晒(普及题) AcWing 110. 防晒 你没有用过的全新OJ 嘿嘿水水题. 题目就是一维坐标轴上给定多个线段,给定多个点,点在线段上造成贡献,点可以重复,问最大贡 ...
- 日志冲突解决方案(基于gradle)
日志冲突解决方案 前提:我使用gradle管理项目 最近在项目中需要用curator客户端操作zookeeper,在maven仓库拉取的jar包导致日志冲突,会报以下的错误: 经常会有如上图2处红色框 ...
- 简单聊一聊JS中的循环引用及问题
本文主要从 JS 中为什么会出现循环引用,垃圾回收策略中引用计数为什么有很大的问题,以及循环引用时的对象在使用 JSON.stringify 时为什么会报错,怎样解决这个问题简单谈谈自己的一些理解. ...
- JavaScript 继承小记
面向对象编程很重要的一个方面,就是对象的继承.A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法.这对于代码的复用是非常有用的. 大部分面向对象的编程语言,都是通过“类”(class) ...
- C语言关键字const作用及其应用
只要学过C语言的,都有知道const这个关键字,知道是用来定义常量的,如果一个变量被const修饰,那么它的值就不能再被改变,那么还有什么其他作用呢? 一.const常用作用 1.修饰局部变量 con ...
- 【转】Java面试题:多继承
招聘和面试对开发经理来说是一个无尽头的工作,虽然有时你可以从HR这边获得一些帮助,但是最后还是得由你来拍板,或者就像另一篇文章“Java 面试题:写一个字符串的反转”所说: 面试开发人员不仅辛苦而且乏 ...