问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

关于并发编程与线程安全的思考与实践

创作时间:
作者:
@小白创作中心

关于并发编程与线程安全的思考与实践

引用
1
来源
1.
https://www.cnblogs.com/Jcloud/p/18474482

并发编程是现代软件开发中不可或缺的一部分,它能够充分利用多核处理器的计算能力,提升程序的执行效率。然而,随着并发程度的提高,线程安全问题也日益凸显。本文将深入探讨并发编程的核心概念、Java内存模型的工作原理,以及如何通过关键字和并发容器来保证线程安全。

一、并发编程的意义与挑战

并发编程的意义在于充分地利用处理器的每一个核,以达到最高的处理性能,从而让程序运行得更快。为了提高计算速率,处理器进行了以下优化:

  1. 硬件升级:引入了多级高速缓存的传统硬件内存架构,以平衡CPU内高速存储器和内存之间数量级的速率差。但这也带来了数据同时存在于高速缓存和主内存中的问题,需要解决缓存一致性问题。

  2. 处理器优化:主要包括编译器重排序、指令级重排序、内存系统重排序。通过单线程语义、指令级并行重叠执行、缓存区加载存储等3种级别的重排序,减少执行指令,从而提高整体运行速度。但在多线程环境中,编译器和CPU指令无法识别多个线程之间存在的数据依赖性,可能影响程序执行结果。

并发编程虽然好处巨大,但要编写一个线程安全且执行高效的代码,需要管理可变共享状态的操作访问,考虑内存一致性、处理器优化、指令重排序等问题。例如,当多个线程对同一个对象的值进行操作时,可能会出现值被更改或不同步的情况,导致结果与理论值大相径庭。因此,在并发编程中保证线程安全是一个容易被忽视但又至关重要的问题。

二、Java并发编程

Java并发采用了共享内存模型,线程之间的通信总是隐式进行的,整个通信过程对程序员完全透明。

2.1 Java内存模型

为了平衡程序员对内存可见性的需求和计算性能的优化,Java定义了Java内存模型(Java Memory Model,JMM)。JMM主要解决的问题是通过制定线程间通信规范,提供内存可见性保证。

根据JMM,线程内创建的局部变量、方法定义参数等只在线程内使用,不会产生并发问题。对于共享变量,JMM规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步访问共享变量。

具体来说,JMM定义了以下规范:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

实现上定义了八种操作:

  1. lock:作用于主内存,把变量标识为线程独占状态。
  2. unlock:作用于主内存,解除独占状态。
  3. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  4. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  5. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  6. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  8. write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

这些操作都满足以下原则:

  • 不允许read和load、store和write操作之一单独出现。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

2.2 Java中的并发关键字

Java基于以上规则提供了volatile、synchronized等关键字来保证线程安全,基本原理是从限制处理器优化和使用内存屏障两方面解决并发问题。如果是变量级别,使用volatile声明任何类型变量,同基本数据类型变量、引用类型变量一样具备原子性;如果应用场景需要一个更大范围的原子性保证,需要使用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需求。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字。

这两个关键字的作用:

  • volatile仅保证对单个volatile变量的读/写具有原子性
  • 锁的互斥执行的特性可以确保整个临界区代码的执行具有原子性
    在功能上,锁比volatile更强大,在可伸缩性和执行性能上,volatile更有优势。

2.3 Java中的并发容器与工具类

2.3.1 CopyOnWriteArrayList

CopyOnWriteArrayList在操作元素时会加可重入锁,以保证写操作是线程安全的。但是每次添加或删除元素都需要复制一份新数组,这在空间上会造成较大的浪费。

public E get(int index) {
    return get(getArray(), index);
}

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

这段代码展示了CopyOnWriteArrayList的添加操作。可以看到,每次添加元素时都会创建一个新的数组副本,然后将新元素添加到新数组中,最后将新数组设置为当前数组。这种设计虽然保证了线程安全,但在频繁写操作的场景下,会带来较大的性能开销。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号