Java并发 - 理论基础

发布时间:2024年01月06日

1. 多核多线程的产生

从计算机的发展史上我们可以了解到,计算机是从单核单线程发展到如今多核多线程

多核多线程的出现是计算机发展史上的一个自然演进,涉及到硬件和软件两个方面的考量。以下是一些主要的原因:

  1. 需求提高: 随着计算机应用的不断扩展和复杂性的增加,对计算能力的需求也在不断提高。单核处理器在性能上有一定的瓶颈,难以满足日益增长的计算需求。

  2. 摩尔定律: Moore’s Law 预测了集成电路上的晶体管数量每两年翻一番,这意味着在同样的芯片面积上可以集成更多的晶体管。然而,随着晶体管数量增加,单核性能提升的幅度逐渐减缓。

  3. 功耗和散热问题: 随着处理器频率的提高,功耗和散热问题变得日益突出。提高频率会导致处理器产生更多的热量,而散热成为一个严重的挑战。多核处理器通过在同一芯片上集成多个核心,能够在相同功耗范围内提供更多的计算能力。

  4. 并行计算需求: 许多应用程序都具有天然的并行性,可以通过并行计算来提高性能。例如,图形处理、科学模拟、数据处理等领域的应用,都可以通过同时处理多个任务或数据来加速计算。

  5. 多任务操作系统: 随着计算机操作系统的发展,多任务和多线程的概念变得日益重要。多核多线程的处理器可以更好地支持并发执行多个任务和线程,提高整个系统的响应性。

  6. 资源利用率: 多核多线程可以更好地利用计算机系统的硬件资源,提高系统整体的效率。在某些情况下,即使单个任务无法充分利用所有核心,多核处理器也可以同时执行多个任务,提高整个系统的吞吐量。

综合考虑以上因素,多核多线程体现了一种在硬件层面上应对性能需求和资源利用率的发展趋势。

2. Java 并发需要解决的问题

  1. 竞态条件(Race Condition): 在多线程环境中,多个线程同时访问和修改共享的数据时可能会导致竞态条件,从而产生不确定的结果。Java 并发提供同步机制,如锁(Locks)和同步块(synchronized blocks),来解决竞态条件问题。

  2. 死锁(Deadlock): 死锁是指两个或多个线程相互等待对方释放锁,从而导致程序无法继续执行。Java 提供了各种工具来预防和解决死锁,如锁的顺序性和 java.util.concurrent 包中的高级并发工具。

  3. 线程安全性: 多线程环境中,多个线程同时访问共享数据可能导致不安全的操作。Java 并发提供了同步机制和线程安全的数据结构,如 ConcurrentHashMap,来确保线程安全性。

  4. 内存可见性: 当多个线程同时访问共享变量时,一个线程对变量的修改可能对其他线程是不可见的。Java 内存模型(Java Memory Model,JMM)定义了一些规则来确保多线程环境下的内存可见性,同时 volatile 关键字也可以用于解决部分内存可见性问题。

  5. 性能优化: 在一些情况下,多线程并发可能导致性能问题,如线程竞争、上下文切换等。Java 并发提供了线程池、并发集合等工具来优化多线程编程的性能。

  6. 复杂的协调和同步: 在一些应用中,多个线程需要协同工作以完成某些任务。Java 提供了 CountDownLatchCyclicBarrierSemaphore 等同步工具来协调线程的执行。

  7. 并发数据结构: Java 提供了一些并发安全的数据结构,如 ConcurrentHashMapConcurrentLinkedQueue,以及 java.util.concurrent 包下的其他集合类,用于在多线程环境中安全地操作数据。

3. 线程不安全的示例

以下是一个线程不安全的示例,多线程对同一个共享数据进行数据操作,那么结果并不是预期结果

public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        UnsafeCounter unsafeCounter = new UnsafeCounter();

        // 创建两个线程,同时增加计数器
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                unsafeCounter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                unsafeCounter.increment();
            }
        });

        // 启动两个线程
        thread1.start();
        thread2.start();

        // 等待两个线程完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终计数器的值
        System.out.println("Final Count: " + unsafeCounter.getCount());
    }
}
Final Count: 16513 // 最终结果总是小于20000

4. JVM内存模型

在 Java 内存模型中,主要涉及到的概念包括主内存、本地内存、工作内存、原子性、可见性、有序性等。下面我们将详细讲解 Java 内存模型的一些规则。

4.1 顺序一致性模型
  • 可见性
  • 原子性
  • 有序性
4.1.1 可见性:CPU缓存

CPU缓存是为了提高计算机系统的性能而引入的,但它也带来了可见性的问题。当一个线程修改了共享变量时,这个修改可能首先发生在线程的本地缓存中而不是主内存中,其他线程可能无法立即看到这个修改。

解决可见性问题的方法之一是使用volatile关键字,它确保了对该变量的写操作会立即被其他线程看到。

public class SharedResource {
    private volatile int sharedVariable;
    
    public void modifySharedVariable(int newValue) {
        sharedVariable = newValue;
    }
}
4.1.2 原子性:操作系统分时

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。在多线程环境中,原子性是确保多个线程同时访问共享变量时不会产生竞态条件的重要特性。

操作系统使用分时机制来调度线程的执行。在一个线程执行时,其他线程处于阻塞状态。这确保了某些操作的原子性,因为在分时机制下,某个线程执行时不会被其他线程中断。

public class AtomicOperation {
    private int sharedVariable;

    public synchronized void atomicModifySharedVariable(int newValue) {
        sharedVariable = newValue;
    }
}
4.1.3 有序性:指令重排序

有序性是指程序执行的结果与代码的顺序相一致。在多线程环境中,由于编译器处理器为了提高执行速度可能会对指令进行重排序,可能导致代码的执行顺序与预期不一致。

public class ReorderingExample {
    private int x = 0;
    private boolean flag = false;

    public void writer() {
        x = 42;
        flag = true;
    }

    public void reader() {
        if (flag) {
            System.out.println(x);
        }
    }
}

在上面的例子中,如果发生了指令重排序,flag可能在x之前被设置为true,导致reader方法输出的值不是预期的结果。

后续文章详细分析:关键字synchronized、volatile

4.2 happens-before 规则

定义了在多线程环境中,操作之间的执行顺序和可见性规则。Happens-before 规则是为了帮助程序员理解和确保正确的多线程程序行为。

  • 程序次序规则(Program Order Rule):在一个线程中,每个操作按照程序代码的顺序执行。
  • 监视器锁规则(Monitor Lock Rule):对一个监视器的解锁操作先于对同一个监视器的加锁操作。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先于对这个变量的读操作。
  • 传递性规则(Transitivity):如果 A 操作先于 B 操作,而 B 操作又先于 C 操作,那么 A 操作必然先于 C 操作。
  • 启动规则(Thread Start Rule):一个线程的启动操作先于该线程的任意操作。
  • Join 规则(Thread Join Rule):在一个线程中,所有的操作先于其他线程成功返回从 Thread.join() 调用中的 join 操作。
  • 中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先于被中断线程检查到中断状态的操作。
  • 终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先于它的 finalize() 方法的开始。
4.3 as-if-seria 规则

它描述了在多线程环境中,对于单线程程序员来说,程序的执行表现应当是按照其在代码中指定的顺序执行的,即 as-if-serial。“as-if-serial” 原则保证了在多线程环境中,程序的执行在其在单线程中的执行的结果是一致的。

  • 程序顺序保证:在多线程环境中,每个线程的操作可能被重排序和优化,但是对于每个线程来说,其操作的执行顺序应当与程序中的代码顺序一致。
  • 单线程语义一致性:程序的执行结果在多线程环境中应当与其在单线程执行时的结果一致。
  • 不改变存在依赖关系的操作顺序:“as-if-serial” 不允许改变存在数据依赖关系的操作的执行顺序。如果操作 A 先于操作 B 发生,而且 A 对 B 有数据依赖关系,那么在多线程环境中,A 依然要先于 B 发生。
4.4 主内存和本地内存
  • 主内存(Main Memory)
    • 主内存是所有线程共享的内存区域,包含程序中的共享变量。
    • 所有线程可以直接读写主内存中的变量。
  • 本地内存(Local Memory)
    • 本地内存是每个线程独享的内存区域,用于存储主内存中的变量的副本。
    • 线程对变量的操作首先在本地内存中进行,然后同步到主内存中。
4.5 主内存和工作内存同步规则
  • 读取操作规则
    • 一个线程对变量的读操作,如果没有在本地内存中找到对应的值,就从主内存中读取。
    • 读取操作时,会将变量的值从主内存拷贝到本地内存中。
  • 写入操作规则
    • 一个线程对变量的写操作,首先将值写入本地内存中,然后同步到主内存中。
    • 写入操作时,会将变量的值从本地内存同步到主内存中。
文章来源:https://blog.csdn.net/qq_43678225/article/details/135428667
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。