博客
关于我
并发框架下的“基础类型”——浅析基本类型、ThreadLocal、原子类的线程安全机制
阅读量:373 次
发布时间:2019-03-05

本文共 2688 字,大约阅读时间需要 8 分钟。

Java线程安全:从单线程到多线程的转变

在学习Java Web开发过程中,尤其是在接触Java线程开发时,常常会遇到一个令人困惑的问题:当多个线程调用同一个方法时,变量会如何变化?这个问题引出了线程安全的概念,包括原子性、有序性和可见性。但在实际开发中,如何选择合适的线程安全类型仍然是一个值得探讨的话题。

1. 阅读代码,理解线程安全问题

我们来看一段代码:

public class Main {    private static final int NUM = 1 << 20; // 定义一个大数 1M    private static ExecutorService threadPool = Executors.newCachedThreadPool();    private static int normalInt = NUM;    private static AtomicInteger atomicInt = new AtomicInteger(NUM);    private static ThreadLocal
localInt = ThreadLocal.withInitial(() -> NUM); public static void main(String[] args) throws InterruptedException { 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会输出非零?

    线程安全问题的一个经典案例是基础类型变量的线程不安全。让我们分析一下原因。

    原因分析

  • 基础类型的迭减操作不是原子操作:迭减的本质是通过创建临时变量来存储“x-1”的结果,然后将这个值赋给x。这意味着迭减操作可以被分解为读取和写入操作。

  • 线程冲突:假设有两个线程同时对同一个变量执行迭减操作:

    • 线程A读取x,计算x-1,存为y1。
    • 线程B读取x,计算x-1,存为y2。
    • 线程A将x写为y1。
    • 线程B将x写为y2。

    最终,x的值只会减少一次。这就是为什么我们会看到非零的输出。

  • 测试结果解读

    测试结果显示,基础类型的迭减操作在多线程环境下会出现3121次插入现象,导致最终变量未能成功减到0。这表明基础类型在并发环境下是不安全的。

    3. ThreadLocal为什么没有改变?

    ThreadLocal类型在我们的测试中没有显示出任何变化。这与ThreadLocal的本质密切相关。

    ThreadLocal的工作原理

    ThreadLocal内部维护了一个HashMap,每个访问ThreadLocal变量的线程都会分配到一个独立的拷贝。因此,主线程最后输出的值实际上没有被任何工作线程修改过,仍然保持原值。

    ThreadLocal与局部变量的区别

    • 局部变量:存储在JVM的栈区,属于线程私有。
    • ThreadLocal:存储在堆区,每个线程有自己的独立拷贝。

    ThreadLocal的优势在于它可以统一管理共享类任务中的变量,避免了传统的锁同步编程的复杂性。

    4. 原子类型的线程安全

    原子类型提供了更高的线程安全性。例如,AtomicInteger的实现源代码如下:

    private static final Unsafe U = Unsafe.getUnsafe();private volatile int value;public final int getAndAdd(int delta) {    return U.getAndAddInt(this, 0, delta);}

    原子类型的实现细节

  • volatile关键字:确保变量的可见性。
  • Unsafe类:利用低层级操作(CAS+自旋)实现原子性。
  • 原子类型的优势在于其内部使用乐观锁机制,性能通常优于传统的锁同步编程。从敏捷开发的角度来看,原子类型在并发环境下的表现更为出色。

    总结

    通过以上分析,我们可以得出以下结论:

  • 基础类型在并发环境下不安全:需要结合锁机制或原子类型进行保护。
  • ThreadLocal适用于共享类任务:提供更高级的线程管理。
  • 原子类型提供了更高的线程安全性:适用于需要原子性和可见性的场景。
  • 在实际开发中,选择合适的线程安全类型取决于具体需求。无论是基础类型+锁,还是原子类型,都有其适用的场景。

    转载地址:http://jyiwz.baihongyu.com/

    你可能感兴趣的文章
    Python简易五子棋
    查看>>
    MySQL8.0.19 JDBC下载与使用
    查看>>
    Vue新建项目——页面初始化
    查看>>
    Cent OS 7.6 服务器软件安装(这篇博客主要是为了方便我配置云主机的)
    查看>>
    MySQL使用系列文章
    查看>>
    Node.js包使用系列(一)——修改NPM全局下载和缓存路径
    查看>>
    TDengine使用(一)——TDengine下载与安装
    查看>>
    ubuntu和windows之间无法复制粘贴
    查看>>
    力扣239. 滑动窗口最大值
    查看>>
    史上最全Vue的组件传值
    查看>>
    CSS position属性static/relative/absolute/fixed/sticky用法总结
    查看>>
    6.14编一个程序,将两个字符串s1和s2比较,不要用strcmp函数。
    查看>>
    如何解决vscode检测到#include错误,请更新includePath。
    查看>>
    Java纯文本文件显示工具制作
    查看>>
    Unity2D Fixed Joint 2D详解
    查看>>
    Unity Shader之路(五)创建第一个顶点/片元着色器?
    查看>>
    L3-008 喊山 (30分) C++ BFS题解
    查看>>
    Web框架——Flask系列之Flask-SQLAlchemy数据库的基本操作(九)
    查看>>
    六、Numpy的使用(详解)
    查看>>
    三、案例:留言板 & url.parse()
    查看>>