Java:并发

什么是线程和进程?线程与进程的关系,区别及优缺点?

为什么要使用多线程呢?

说说线程的生命周期和状态?

6 种状态( NEW 、 RUNNABLE 、 BLOCKED 、 WAITING 、 TIME_WAITIN G 、 TERMINATED )。 🌈 拓展:在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态。

什么是线程死锁?如何避免死锁?如何预防和避免线程死锁?

线程死锁是指两个或多个线程相互持有对方所需的资源,并且在等待对方释放资源时无限期地阻塞的情况。简单来说,线程 A 持有资源 X,等待资源 Y,而线程 B 持有资源 Y,等待资源 X,这样两个线程就陷入了僵局,无法继续执行。

要避免死锁,可以采取以下几种方法:

  1. 避免使用多个锁: 尽量减少在一个线程中需要持有多个锁的情况。如果确实需要使用多个锁,尽量按照相同的顺序获取锁,以减少死锁的可能性。
  2. 避免嵌套锁: 尽量避免在持有一个锁的时候去尝试获取另一个锁,这样容易造成死锁。
  3. 避免阻塞等待: 尽量减少线程在等待锁时的阻塞时间,可以使用非阻塞的锁或者使用超时机制来尝试获取锁。
  4. 使用固定顺序获取锁: 如果必须使用多个锁,确保所有线程都按照相同的顺序获取锁,以避免循环等待的情况。
  5. 使用可重入锁: 可重入锁允许线程重复获取已经持有的锁,避免了死锁的发生。
  6. 使用死锁检测工具: 一些开发工具和框架提供了死锁检测的功能,可以帮助检测和解决潜在的死锁问题。
  7. 合理设计资源获取顺序: 在设计程序时,应该尽量避免循环等待资源的情况,合理规划资源的获取顺序。
  8. 尽量简化代码逻辑: 复杂的代码逻辑容易引入死锁,尽量简化和优化代码,减少出现死锁的可能性。

    乐观锁和悲观锁

  9. 悲观锁(Pessimistic Locking):

悲观锁假设会发生并发访问冲突,因此在访问共享资源之前会先加锁,确保在锁定期间其他线程无法访问该资源。悲观锁的典型实现是数据库的行级锁或者表级锁。

  • 特点

    • 在整个访问过程中,共享资源一直被锁定,其他线程无法访问,直到当前线程释放锁。
    • 可以确保数据的完整性和一致性,但是在高并发环境下可能会降低性能,因为会造成线程阻塞和等待。
  • 适用场景

    • 当对数据修改频率较高,且需要保证数据一致性和完整性时使用。
    • 当并发冲突概率较高,或者事务执行时间较长时适用。
    1. 乐观锁(Optimistic Locking):

乐观锁假设并发访问冲突的概率较低,在访问共享资源之前不加锁,而是在更新资源时检查是否发生了并发修改。乐观锁的典型实现是使用版本号或时间戳来判断是否发生了并发修改。

  • 特点

    • 不加锁,允许多个线程同时访问共享资源,减少了线程阻塞和等待的情况,提高了并发性能。
    • 更新资源时需要先检查是否发生了并发修改,通常是比较版本号或时间戳,如果没有发生修改则更新成功,否则进行回滚或者重试。
  • 适用场景

    • 当对数据的读取操作远远大于写入操作,且并发冲突概率较低时使用。R>W
    • 当并发冲突发生概率较低,且可以容忍一定的数据冲突时适用。

    乐观锁和悲观锁的区别

  • 并发控制策略

    • 乐观锁(Optimistic Locking):乐观锁假设并发冲突的概率较低,因此在访问共享资源之前不加锁,而是在更新资源时进行检查,如果发现资源已经被其他线程修改,则会进行回滚或者重试。乐观锁的典型实现是使用版本号或者时间戳来判断是否发生了并发修改。
    • 悲观锁(Pessimistic Locking):悲观锁假设并发冲突的概率较高,因此在访问共享资源之前会先加锁,确保在锁定期间其他线程无法访问该资源。悲观锁的典型实现是使用数据库的行级锁或者表级锁。
  • 加锁时机

    • 乐观锁:在更新资源时进行检查是否发生了并发修改,不加锁让多个线程同时访问共享资源。
    • 悲观锁:在访问共享资源之前加锁,确保在锁定期间其他线程无法访问该资源。
  • 性能影响

    • 乐观锁:不加锁,允许多个线程同时访问共享资源,因此性能较高。但在检查并发修改时可能需要进行回滚或者重试,可能会增加系统负担。
    • 悲观锁:在访问共享资源之前加锁,因此可能会降低系统的并发性能,因为会造成线程阻塞和等待。
  • 适用场景

    • 乐观锁:适用于对数据的读取操作远远大于写入操作,且并发冲突概率较低的场景。
    • 悲观锁:适用于频繁发生并发冲突、对数据一致性要求较高的场景

    如何实现乐观锁?

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这 里需要格外注意。

版本号机制

@Service
@Transactional
public class MyService
{
    @Autowired private MyEntityRepository myEntityRepository;
    public void updateData(Long id, String newData)
    {
        MyEntity entity = myEntityRepository.findById(id).orElse(null);
        if (entity != null)
        { // 获取当前数据的版本号
            int currentVersion = entity.getVersion();
            // 执行更新操作前,再次查询数据库获取最新的版本号
            MyEntity updatedEntity = myEntityRepository.findById(id).orElse(null);
            if (updatedEntity != null && updatedEntity.getVersion() == currentVersion)
            {
                // 如果版本号一致,则执行更新操作
                updatedEntity.setData(newData);
                myEntityRepository.save(updatedEntity);
            }
            else
            {
                // 如果版本号不一致,则表示数据已被其他线程修改,需要进行回滚或者重试
                throw new OptimisticLockException("Data has been modified by another thread.");
            }
        }
    }
}

CAS 了解么?原理?

CAS(Compare and Swap,比较并交换)是一种乐观锁技术,用于实现多线程环境下的原子操作。它是一种基于硬件的原子操作,在现代处理器中有专门的指令集支持。

CAS 操作包括三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值等于预期原值,则将该位置的值更新为新值,否则不做任何操作。整个过程是原子的,即在同一时刻只有一个线程能够修改该值。

CAS 操作的原理是利用了 CPU 提供的原子操作指令,例如 cmpxchg(在 x86 架构下)或者 compareAndSwap(在 Java 中的实现)。这些指令能够实现在同一时刻对内存位置的读取、比较和修改操作,确保在多线程环境下的线程安全性。

CAS 操作的基本流程如下:

  1. 读取内存位置 V 的当前值,记为 A。
  2. 比较内存位置 V 的当前值(A)与预期原值(B)是否相等。
  3. 如果相等,则将内存位置 V 的值更新为新值(C),操作成功,否则操作失败。
  4. 返回操作结果,如果操作成功,则返回 true,否则返回 false。

CAS 操作的优点是避免了使用锁(例如 synchronized)带来的性能开销和线程阻塞,因为它是一种非阻塞的并发控制方式。但是 CAS 也有一些缺点,例如 ABA 问题(即在两次读取之间,值被其他线程改变为原来的值),以及可能出现的自旋操作(即不断尝试直到成功,可能会消耗大量的 CPU 资源)。

在 Java 中,CAS 操作主要由 java.util.concurrent.atomic 包中的原子类(例如 AtomicIntegerAtomicLongAtomicReference 等)来实现,这些类提供了对基本数据类型的原子操作。CAS 的使用可以帮助实现线程安全的并发编程,例如在计数器、锁、队列等数据结构中的应用。

  1. 读取内存位置的当前值(A): 线程首先读取内存位置的当前值,这个值会被保存下来,并且在后续的比较中被使用。
  2. 计算预期值(B): 线程根据业务逻辑计算出预期的新值(通常是根据当前值计算出的新值)。
  3. 读取内存位置的当前值(再次读取): 线程再次读取内存位置的当前值,这个值会用于与预期值进行比较。
  4. 比较内存位置的当前值与预期值: 线程将之前保存的当前值(A)与第三步读取的当前值进行比较,如果相等,则表示内存位置的值没有在此期间被其他线程修改过,可以执行更新操作;如果不相等,则表示内存位置的值已经被其他线程修改过,更新操作失败。
  5. 如果相等,则执行更新操作: 如果比较结果为相等,则线程执行更新操作,将预期的新值(B)写入内存位置,并完成 CAS 操作;如果比较结果为不相等,则更新操作失败,线程需要根据业务逻辑进行重试或者其他处理。

    乐观锁存在哪些问题?

ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作
ABA:是因为CAS线程不会阻塞,线程一致自旋。别的线程又把值修改为A
循环时间长开销大: 在乐观锁的实现中,如果发生了冲突,需要进行重试操作,通常是通过循环重试来实现。如果冲突频繁发生,会导致线程不断进行重试,增加了系统的开销。
只能保证一个共享变量的原子操作: 乐观锁通常只能保证对单个共享变量的原子操作,对于多个共享变量之间的复合操作,需要额外的措施来保证原子性,增加了实现的复杂性。

什么是 ABA 问题?ABA 问题怎么解决?

所谓 ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。ABA 问题的解决思路是在变量前面追加 上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类 AtomicStamped Reference 类来解决 ABA 问题。

解决 ABA 问题的常见方式是使用版本号或者时间戳来确保数据的一致性。具体方法如下:

  1. 版本号(Versioning): 在进行乐观锁比较时,不仅需要比较共享变量的值,还需要比较版本号。每次对共享变量进行更新时,都会增加版本号,这样即使值被改变过多次,版本号仍然会随着每次更新而改变,从而避免了 ABA 问题。
  2. 时间戳(Timestamp): 类似于版本号,可以使用时间戳来记录每次更新操作的时间,确保更新的原子性。每次更新时,会将时间戳作为一部分进行比较,以确保操作的原子性。
  3. 引入额外信息: 可以在每次更新时引入额外的信息,例如增加一个序列号或者随机数,确保每次更新操作都具有唯一性,从而避免 ABA 问题。
  4. 使用带有ABA问题检测的原子操作: 有些并发库提供了带有 ABA 问题检测的原子操作,例如 AtomicStampedReference 类。这些原子操作在进行更新时会比较引用和标记,从而避免了 ABA 问题。

    JMM

并发编程的三个重要特性

原子性、可见性、有序性

什么是 JMM?为什么需要 JMM?

对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除 了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这 个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强 程序可移植性的。

JMM 是如何抽象线程和主内存之间的关系?

JMM(Java Memory Model,Java 内存模型)是描述了 Java 程序中多线程并发访问共享内存时的内存可见性、操作顺序和原子性等行为规范。JMM 定义了 Java 虚拟机(JVM)如何在不同线程之间共享数据,并规定了各种情况下内存操作的行为。

需要 JMM 的原因主要是:

  1. 多线程并发访问共享内存: Java 程序通常是多线程并发执行的,不同线程之间可能会同时访问共享内存中的数据。因此,需要一种机制来确保线程之间的数据访问的可见性、有序性和原子性,以保证程序的正确性和可靠性。
  2. 跨平台的特性: Java 是一种跨平台的编程语言,同一个 Java 程序可以在不同的操作系统和硬件平台上运行。JMM 的存在可以确保 Java 程序在不同平台上的并发行为是一致的,提高了程序的可移植性和稳定性。
  3. 提供内存操作规范: JMM 规定了不同线程之间共享内存操作的行为规范,包括内存可见性、操作顺序和原子性等方面,为 Java 开发人员提供了一种标准化的内存操作方式,简化了多线程编程的复杂性。

总的来说,JMM 的存在为 Java 并发编程提供了一种规范和标准,保证了多线程并发访问共享内存的正确性和可靠性,提高了 Java 程序的稳定性和可移植性。

Java 内存区域和 JMM 有何区别?

Java 内存区域(Java Memory Area)和 Java 内存模型(Java Memory Model,JMM)是 Java 虚拟机(JVM)中两个不同的概念,它们分别描述了 JVM 内存的不同方面。

  1. Java 内存区域(Java Memory Area):

    • Java 内存区域是指 Java 虚拟机在运行过程中管理的内存区域,它包括了 JVM 运行时数据区域的各个部分。
    • Java 内存区域主要包括了程序计数器、Java 虚拟机栈、本地方法栈、堆、方法区等多个部分,每个部分都有自己的作用和生命周期。
    • Java 内存区域描述了 JVM 运行时数据区域的组成结构和功能,以及各个区域的作用和特点,与具体的 JVM 实现相关。
  2. Java 内存模型(Java Memory Model,JMM):

    • Java 内存模型是描述了 Java 程序中多线程并发访问共享内存时的内存可见性、操作顺序和原子性等行为规范。
    • Java 内存模型定义了 Java 虚拟机如何在不同线程之间共享数据,并规定了各种情况下内存操作的行为。
    • Java 内存模型描述了 Java 程序中多线程并发访问共享内存的行为规范,与 Java 编程语言相关。

happens-before 原则是什么?为什么需要 happens-before 原则?

"happens-before" 原则是 Java 内存模型(Java Memory Model,JMM)中的一个重要概念,用于描述多线程环境下对共享变量的内存可见性以及操作顺序的规范。

"happens-before" 原则规定了一组规则,指明了在多线程环境中,如果一个操作 "happens-before" 另一个操作,那么前一个操作对共享变量的修改对后一个操作是可见的。具体来说,如果操作 A "happens-before" 操作 B,那么操作 A 的结果对操作 B 是可见的。

需要 "happens-before" 原则的原因主要有以下几点:

  1. 确保内存可见性: 在多线程环境下,不同线程对共享变量的修改可能会存在缓存不一致的问题,导致线程之间无法看到彼此的修改。"happens-before" 原则通过规定操作顺序,确保了对共享变量的修改对其他线程是可见的,保证了内存可见性。
  2. 规范操作顺序: 在多线程环境下,如果没有规定操作的执行顺序,那么不同线程之间的操作可能会乱序执行,导致程序行为不确定。"happens-before" 原则规定了一组操作顺序的规则,使得程序行为更加可预测和可理解。
  3. 保证并发编程正确性: 在并发编程中,正确地控制共享变量的访问和操作顺序至关重要。"happens-before" 原则提供了一个严格的规范,帮助开发人员正确地编写并发程序,避免出现内存可见性和操作顺序相关的 bug。

线程池

线程池有哪几种,各种线程池的优缺点,线程池的重要参数、线程池的执行流 程、线程池的饱和策略、如何设置线程池的大小等等。

AQS

AQS 是什么?AQS 的原理是什么?

Semaphore 有什么用?Semaphore 的原理是什么?

CountDownLatch 有什么用?CountDownLatch 的原理是什么?用过 CountDownLatch 么?什么场景下用的?

CyclicBarrier 有什么用?CyclicBarrier 的原理是什么?

tag(s): none
show comments · back · home
Edit with Markdown