
并发框架下的“基础类型”——浅析基本类型、ThreadLocal、原子类的线程安全机制
发布日期:2021-05-08 09:34:30
浏览次数:10
分类:精选文章
本文共 3234 字,大约阅读时间需要 10 分钟。
写在前面
在接触java-web开发的时候,常常会思考多个会话在调用同一个方法的时候会发生的事情。其实自然而然就会想到学习java时常提到的“线程安全”概念,原子性、顺序性、可见性。但是对于同样是线程安全的变量或者说容器,它们的安全特性是否有区别,换言之,在特定的场景下,即使我们不从性能角度出发思考问题,我们也仍然要使用不同的线程安全类型。
正文
1. 阅读一段代码
public class Main { // 定义一个大数 1M private static final int NUM = 1 << 20; private static ExecutorService threadPool = Executors.newCachedThreadPool(); private static int normalInt = NUM; private static AtomicInteger atomicInt = new AtomicInteger(NUM); private static ThreadLocallocalInt = ThreadLocal.withInitial(() -> NUM); public static void main(String[] args) throws InterruptedException { // 切回主线程用的latch CountDownLatch latch = new CountDownLatch(NUM); long begin = System.currentTimeMillis(); // 计时开始 for (int i = 0; i < NUM; i++) { threadPool.execute(() -> { //分别用对应方法迭减 normalInt--; atomicInt.getAndAdd(-1); localInt.set(localInt.get() - 1); // latch计数 latch.countDown(); }); } long time = System.currentTimeMillis() - begin; // 计时结束 latch.await(); // 等待任务完成切回主线程 System.out.println("Origin: 2^20 = " + NUM + " normal: " + normalInt + " atomic: " + atomicInt.get() + " local: " + localInt.get()); System.out.println("time: " + time / 1000D + "s"); threadPool.shutdown(); }}
- 在这段代码中,我们创建了1M个相同的任务,利用线程池多线程执行。每个任务都是对变量进行指定方法对三种成员分别迭减。观察最后输出的结果。对于这种计数场景,我们期望最后得到的结果为0。
- 输出:
Origin: 2^20 = 1048576 normal: 3121 atomic: 0 local: 1048576time: 4.973s
2. 基本类型int为什么输出了非0
- 首先要说的是,迭减(--)运算并不是一个原子操作,迭减的本质(class原码)是创建一个临时变量来存储“x - 1”的结果,然后再把这个值赋给x,这个过程是可分的。
- 我们假设有两个线程对同一个变量做迭减运算。
- 线程A:读x,计算x-1,存为y1
- 线程B:读x,计算x-1,存为y2
- 线程A:写x为y1
- 线程B:写x为y2
- 显而易见,y1和y2都是相同的值,迭减操作表现在最后的x的值上则是只减了一次。问题发生的本质原因就是原子性被破坏了,线程B的读写操作被A的写操作插入,导致B的写操作并不是基于所读取的原值进行的。
- 按照测试代码输出的结果来看,类似的插入发生了3121次,导致了最后没有成功清0的情况。
- 总结:基础类型线程不安全,如果要使用基础类型,在并发场景下则应当使用恰当的锁机制来保护基础类型变量。
3. ThreadLocal类型为什么没有任何改变?
- 这就要谈到ThreadLocal的本质了,ThreadLocal内部维护了一个HashMap,这个HashMap让每一个访问ThreadLocal变量的线程分配到一个各自独立的拷贝。也就是说,每个线程生命周期结束之后,对于各自操作的对象的引用的维护也就消失了,在主线程最后输出的值实际上没有被任何工作线程操作过,自然就会输出原值。
- 利用ThreadLocal进行操作很像是在Runnable实现类中写了一个局部变量,但其执行的本质并不同,也能更为优雅地实现方法层面对各线程任务的统一控制:
- 局部变量存储在JVM的栈区,而ThreadLocal则是为每一个线程在堆区维护了同一个HashMap中的不同元素。
- ThreadLocal将变量统一维护在了一起,这方便对方法层次或者实例层次对一个语义的变量进行统一管理。
- ThreadLocal提供了可以自定义的初始化方法。
- 总结:ThreadLocal是线程安全的,但是对于共享类任务(可见性)并不适用。对于不强调共享的任务来说,局部变量已经足够安全,但是为了统一管理这些变量至堆区,或者说解藕方法实现与Runnable实现,ThreadLocal仍然有应用的必要性。
4. 原子类型提供了线程安全
- 如测试代码输出的结果,原子类型保障了这个强调可见性和原子性的任务的线程安全。其本质是原子类型的主要方法是原子的。
- 我们不妨来看一下原子类(AtomicInteger是其中之一)的实现源码:
private static final Unsafe U = Unsafe.getUnsafe();private volatile int value;public final int getAndAdd(int delta) { return U.getAndAddInt(this, VALUE, delta);}
- 原子类维护了一个volatile变量保证其可见性,而后利用unsafe方法来具体执行操作,unsafe方法内部:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v;}
- unsafe方法内部利用了CAS+自旋的方式实现了一个乐观锁,用乐观锁的思想来完成值的读写原子性保障。
- 结论:原子类的方法可以保障线程安全,其实现本质是乐观锁+volatile关键字。在大多数情况下,原子类对比基本类型+锁的方式效率更高,因为原子类内部是乐观锁实现。换言之,从敏捷开发的角度来思考,原子类型永远值得被在并发环境下优先考虑 。从性能角度出发,到底是选择基本类型+锁,还是原子类,本质上是一个悲观锁和乐观锁机制的抉择。
发表评论
最新留言
能坚持,总会有不一样的收获!
[***.219.124.196]2025年04月09日 18时44分41秒
关于作者

喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!
推荐文章
phthon基本语法——温习
2021-05-08
sleep、wait、yield、join——简介
2021-05-08
web项目配置
2021-05-08
VTK:相互作用之MouseEventsObserver
2021-05-08
VTK:相互作用之PickableOff
2021-05-08
VTK:相互作用之Picking
2021-05-08
VTK:Medical之MedicalDemo2
2021-05-08
VS配置属性表,保存Opencv配置信息
2021-05-08
c语言(基本数据类型)实参与形参传值 用汇编理解
2021-05-08
输入端噪声容限
2021-05-08
vue——this.$route 与 this.$router
2021-05-08
基于单片机可控音乐流水灯控制设计-全套资料
2021-05-08
基于单片机简易信号误差分析设计-全套资料
2021-05-08
基于单片机简易洗衣机系统仿真设计-全套资料
2021-05-08
基于单片机简易脉搏测量仪系统设计-毕设课设资料
2021-05-08
并发框架下的“基础类型”——浅析基本类型、ThreadLocal、原子类的线程安全机制
2021-05-08
Android Studio同步Gradle失败的解决办法
2021-05-08
VHDL代码风格
2021-05-08