/**PageBeginHtml Block Begin **/ /***自定义返回顶部小火箭***/ /*生成博客目录的JS 开始*/ /*生成博客目录的JS 结束*/

Java并发编程与高并发解决方案(完整无密)


一:前言

1:课程导学


image


image



image



image

image





image



image







image




image




image




















2:并发编程初体验


image

Pom.xml内容信息



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.0-SNAPSHOT</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.alan</groupId>
	<artifactId>springbootStudying</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springbootStudying</name>
	<description>Demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.16.10</version>
		</dependency>

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>23.0</version>
		</dependency>

		<dependency>
			<groupId>joda-time</groupId>
			<artifactId>joda-time</artifactId>
			<version>2.9</version>
		</dependency>

		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.8.2</version>
		</dependency>

		<!-- hystrix -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-hystrix</artifactId>
		</dependency>

		<!-- hystrix dashboard -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>


	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<releases>
				<enabled>false</enabled>
			</releases>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<releases>
				<enabled>false</enabled>
			</releases>
		</pluginRepository>
	</pluginRepositories>

</project>

CountExample1

package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.example.count;


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class CountExample1 {

    //请求总数
    private static int clientTotal = 5000;
    //同时并发执行的线程数
    private static int threadTotal = 200;
    private static long count = 0;

    public static void main(String[] args) {
        //线程池
        ExecutorService exec = Executors.newFixedThreadPool(threadTotal);
        //信号量              模拟客户端发出 5000个请求  同一时间内允许200个请求同时执行
        final Semaphore semaphore = new Semaphore(threadTotal);
        for (int i = 0; i < clientTotal; i++) {
            exec.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                }catch (Exception e) {
                    log.error("exception", e);
                    throw new RuntimeException(e);
                }
            });
        }
        exec.shutdown();
        log.info("total count: {}", count);
    }
    private static void add() {
        count++;
    }
}


执行情况:

image

image

image





MapExample


package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.example.count;

import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class MapExample {

    private static Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    private static int threadNum = 200;
    private static int clientNum = 50000;

    public static void main(String[] args) throws Exception {
        //线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //信号量
        final Semaphore semaphore = new Semaphore(threadNum);
        //模拟请求
        for (int index = 0; index < threadNum; index++) {
            final int threadNum = index;
            exec.execute(()->{
                try {
                    semaphore.acquire();
                    func(threadNum);
                    semaphore.release();
                }catch (Exception e) {
                    log.error("exception", e);
                    throw new RuntimeException(e);
                }
            });
        }
        exec.shutdown();
        log.info("size:{}",map.size());
    }

    public static void func(int threadNum) {
        map.put(threadNum,threadNum);
    }

}



image







3:并发与高并发基本概念

image

image


image


image



image





image



image

















二、课程准备

1、并发编程的基础---CPU多级缓存及缓存一致性

image


image



image


image


1. MESI 协议简介

MESI 是一种用于保证多个 CPU cache 之间缓存共享数据一致性的协议 。它定义了缓存行(cache line)的四种状态:

  • M(Modified,已修改):该缓存行中的数据已被修改,与主存中的数据不一致,且该缓存行只在当前 CPU 缓存中存在。
  • E(Exclusive,独占):该缓存行中的数据与主存一致,且只在当前 CPU 缓存中存在。
  • S(Shared,共享):该缓存行中的数据与主存一致,且在多个 CPU 缓存中都存在。
  • I(Invalid,无效):该缓存行中的数据无效。
2. 状态转换分析
  • 左侧状态转换图:不同颜色箭头代表不同操作下的状态转换。比如,local write(本地写)操作可能会让状态从 S 转换到 M 等 ,具体转换取决于当前缓存行状态。local read(本地读)、remote read(远程读,其他 CPU 读取该缓存行数据 )、remote write(远程写,其他 CPU 修改该缓存行数据 )也会引发相应状态转换。
  • 右侧表格:表格交叉处表示从行状态转换到列状态是否可行。绿色对勾(√)表示可行,红色叉号(×)表示不可行 。比如 M 状态不能转换为自身(M - M 是 × ),因为一个缓存行处于已修改状态时,不会再维持原有的已修改状态(没有这种操作逻辑 );而 I 状态可以转换为 M、E、S、I 任何状态,因为无效状态下可以通过各种操作重新载入有效数据。


一、MESI 协议概述

在现代多核 CPU 系统中,每个 CPU 核心都有自己的高速缓存(Cache)。当多个核心同时访问共享内存数据时,就可能出现缓存中数据不一致的问题。MESI 协议就是为了解决这个问题而设计的,它通过定义缓存行的四种状态以及相应的状态转换规则,确保各个 CPU 核心缓存中的共享数据与主存数据保持一致。

二、缓存行的四种状态
  1. M(Modified,已修改)
    • 含义:处于该状态的缓存行,其中的数据已经被当前 CPU 核心修改过,与主存中的数据不一致。并且在整个系统中,这个缓存行只存在于当前 CPU 的缓存里,其他 CPU 缓存中没有该缓存行的有效副本。
    • 举例:假设 CPU1 的缓存中有一个缓存行处于 M 状态,它存储了变量 A 的值,且这个值已经被 CPU1 修改过。此时主存中变量 A 的值还是旧值,而且 CPU2、CPU3 等其他 CPU 的缓存中都没有这个缓存行的有效版本。
    • 作用:这种状态保证了当前 CPU 核心对数据的修改可以暂时不写回主存,直到合适的时机(比如缓存行被替换等情况)再进行写回操作,从而减少对主存的写操作次数,提高系统性能。
  2. E(Exclusive,独占)
    • 含义:此状态下的缓存行数据与主存中的数据一致,并且在整个系统中,该缓存行只存在于当前 CPU 的缓存中,其他 CPU 缓存中不存在该缓存行的副本。
    • 举例:若 CPU2 从主存中读取了变量 B 到自己的缓存行中,此时该缓存行处于 E 状态。这意味着在这一时刻,只有 CPU2 的缓存拥有变量 B 的这个缓存行,且其内容和主存一致。
    • 作用:当 CPU 核心需要对数据进行修改时,如果缓存行处于 E 状态,就可以直接修改,而不需要先与其他 CPU 进行数据同步等额外操作,提高了数据访问和修改的效率。
  3. S(Shared,共享)
    • 含义:处于 S 状态的缓存行,其数据与主存一致,并且在多个 CPU 的缓存中都存在该缓存行的副本。
    • 举例:多个 CPU 核心都需要读取变量 C 的值,那么变量 C 的缓存行就会被加载到多个 CPU 的缓存中,此时这些缓存行都处于 S 状态。
    • 作用:在多个 CPU 核心都只是读取数据而不修改的情况下,共享状态可以让多个 CPU 缓存共享同一个缓存行数据,减少了数据在缓存中的重复存储,提高了缓存的利用率。
  4. I(Invalid,无效)
    • 含义:该状态表示缓存行中的数据是无效的,不能被 CPU 核心使用。
    • 举例:如果一个缓存行原本处于 S 状态,此时有另一个 CPU 核心对该缓存行对应的主存数据进行了修改,那么其他 CPU 缓存中该缓存行就会变为 I 状态。
    • 作用:无效状态保证了缓存中的数据不会出现错误使用的情况,当 CPU 核心需要使用该缓存行数据时,会重新从主存或其他有效的缓存副本中获取最新数据。
三、状态转换
  1. 左侧状态转换图
    • local read(本地读):当 CPU 核心执行本地读操作时,如果缓存行处于 I 状态,就需要从主存或其他拥有有效副本的 CPU 缓存中读取数据,然后根据情况将缓存行状态转换为 E(如果其他缓存中没有该缓存行副本)或 S(如果其他缓存中也有该缓存行副本)。例如,CPU1 要读取变量 D,其缓存行当前处于 I 状态,从主存读取后,若其他 CPU 缓存中无此副本,该缓存行变为 E 状态;若有其他副本,则变为 S 状态。
    • local write(本地写):若缓存行处于 E 状态,CPU 核心可以直接进行写操作,状态变为 M;若处于 S 状态,需要先将其他 CPU 缓存中该缓存行置为 I 状态(通过总线嗅探等机制通知其他 CPU ),然后进行写操作,状态变为 M;若处于 I 状态,则先从主存读取数据(可能会转换为 E 或 S 状态 ),再进行写操作变为 M。比如 CPU2 要写变量 E,其缓存行处于 S 状态,就需先让其他 CPU 缓存中该缓存行无效,再修改并将自身状态变为 M。
    • remote read(远程读):当其他 CPU 核心进行远程读操作时,如果当前缓存行处于 M 状态,需要先将修改后的数据写回主存,然后状态可能转换为 S(如果有多个 CPU 都要读取该缓存行数据 );如果处于 E 状态,状态转换为 S。例如,CPU3 要远程读 CPU1 缓存中处于 M 状态的缓存行数据,CPU1 需先将数据写回主存,之后该缓存行可能变为 S 状态。
    • remote write(远程写):当其他 CPU 核心进行远程写操作时,无论当前缓存行处于何种状态(M、E、S ),都要将当前缓存行状态置为 I,因为数据已被其他 CPU 修改,当前缓存行数据不再有效。比如 CPU4 远程写 CPU2 缓存中处于 E 状态的缓存行数据,CPU2 中该缓存行就会变为 I 状态。
  2. 右侧表格
    • 表格展示了不同状态之间转换的可行性。例如,M 状态不能转换为自身(M - M 为 × ),这是因为一旦缓存行处于已修改状态,不会再通过任何合法操作维持原有的已修改状态,后续会根据不同情况转换为其他状态(如被替换时写回主存变为 I 状态等 )。
    • 从 I 状态可以转换到 M、E、S、I 中的任何状态。因为 I 状态表示无效,当 CPU 核心需要使用该缓存行数据时,根据具体的操作(读或写,以及其他缓存的情况 ),可以重新将其变为有效状态(M、E、S )或者继续保持无效(I ,比如后续没有对该缓存行的操作 )。



一、MESI 协议底层实现机制
  1. 总线嗅探(Bus Snooping)
    • 原理:在基于 MESI 协议的系统中,各个 CPU 核心通过系统总线进行通信。每个 CPU 核心的缓存控制器会 “嗅探” 总线上传输的内存事务信息。例如,当一个 CPU 核心要对处于共享状态(S)的缓存行进行写操作时,它会先在总线上广播一个写请求。其他 CPU 核心的缓存控制器通过嗅探到这个请求,就会知道自己缓存中对应的共享缓存行需要被置为无效(I)状态。这种机制确保了各个 CPU 核心能实时感知其他核心对共享数据的操作,从而维护缓存一致性。
    • 举例:假设有三个 CPU 核心(CPU A、CPU B、CPU C),它们的缓存中都有变量 X 的共享缓存行(S 状态)。当 CPU A 要对变量 X 进行写操作时,它会在总线上发送写事务信号。CPU B 和 CPU C 的缓存控制器通过嗅探总线,检测到这个写事务,然后将自己缓存中变量 X 的缓存行状态置为无效(I)。这样,只有 CPU A 的缓存行可以进行写操作并转换为已修改状态(M),保证了数据一致性。
    • 局限性:随着 CPU 核心数量的增加,总线上的事务流量会增大,总线竞争加剧,可能导致系统性能下降。而且,总线嗅探机制需要每个缓存控制器不断监听总线,增加了硬件的复杂度和功耗。
  2. 目录协议(Directory - based Protocol)
    • 原理:目录协议引入了一个目录结构,用于记录系统中每个缓存行的状态信息以及所在位置(即哪个 CPU 核心拥有该缓存行)。当发生缓存操作时,不再单纯依赖总线广播,而是先查询目录。例如,当一个 CPU 核心要读取某个缓存行时,它会先查询目录,看是否有其他 CPU 核心拥有该缓存行的有效副本。如果有,就可以直接从对应的 CPU 核心缓存中获取数据,而不需要访问主存。这种方式减少了总线上的通信量。
    • 举例:系统中有一个目录记录着变量 Y 的缓存行状态和位置信息。当 CPU D 要读取变量 Y 时,它先查询目录,发现 CPU E 的缓存中有变量 Y 的独占(E)状态缓存行。此时,CPU D 可以直接向 CPU E 请求数据,而不是从主存读取。CPU E 将数据发送给 CPU D 后,两者的缓存行状态可能会根据具体操作转换为共享(S)状态。
    • 优势与不足:相比总线嗅探,目录协议能更好地适应多核系统,减少总线流量。但它也增加了硬件复杂度,因为需要额外的存储空间来维护目录信息,并且目录查询操作也会带来一定的延迟。
二、MESI 协议对系统性能的影响
  1. 减少主存访问次数
    • 机制:MESI 协议通过允许 CPU 核心在本地缓存中直接读取和修改数据,减少了对主存的频繁访问。当缓存行处于独占(E)或已修改(M)状态时,CPU 核心可以在本地进行写操作而无需立即写回主存;当处于共享(S)状态时,多个 CPU 核心可以直接从缓存中读取数据,避免了从主存读取的高延迟。
    • 性能提升效果:例如在一个多线程应用程序中,多个线程可能会频繁读取共享数据。如果没有 MESI 协议,每个线程每次读取数据都可能需要访问主存,而主存的访问延迟通常比缓存高几十甚至上百倍。通过 MESI 协议,数据可以在缓存中共享,大大降低了主存访问频率,提高了程序的执行速度。据测试,在一些特定的多线程计算密集型应用中,采用 MESI 协议的缓存机制可以将程序性能提升 30% - 50%。
  2. 缓存一致性开销
    • 开销来源:虽然 MESI 协议能提高性能,但维护缓存一致性也会带来一定开销。例如,当缓存行状态转换时,需要通过总线进行通信,通知其他 CPU 核心缓存行状态的变化,这会占用总线带宽;同时,CPU 核心的缓存控制器在处理状态转换逻辑时,也会消耗一定的计算资源。
    • 对性能的影响:在高并发场景下,大量的缓存行状态转换可能导致总线拥塞,从而增加数据访问延迟。此外,频繁的状态转换也会使 CPU 核心的缓存控制器负担加重,影响整体性能。例如在一些大数据处理应用中,由于数据共享频繁,缓存一致性开销可能会抵消一部分性能提升,甚至在极端情况下导致性能下降。
三、MESI 协议与现代编程模型
  1. 多线程编程
    • 数据竞争问题:在多线程编程中,多个线程可能会同时访问共享数据。如果没有合适的机制来保证数据一致性,就会出现数据竞争问题,导致程序结果不可预测。MESI 协议从硬件层面为多线程编程提供了一定的数据一致性保障。例如,在 Java 多线程编程中,当多个线程访问共享变量时,CPU 的 MESI 协议可以确保每个线程看到的共享变量值是一致的(在一定程度上),避免了一些简单的数据竞争问题。
    • 同步原语的优化:基于 MESI 协议,编程语言中的一些同步原语(如锁机制)可以进行优化。例如,在实现自旋锁时,可以利用 MESI 协议中缓存行状态转换的特性。当一个线程获取自旋锁失败并进入自旋等待时,它可以通过检测缓存行状态变化来判断锁是否可以被获取,而不需要频繁地访问主存或进行复杂的内存同步操作,从而提高了自旋锁的性能。
  2. 并行计算框架
    • 分布式缓存一致性:在一些分布式并行计算框架(如 Apache Spark 等)中,虽然主要处理的是分布式节点间的数据一致性,但也可以借鉴 MESI 协议的思想。例如,在 Spark 的内存计算模块中,不同任务在同一个节点上执行时,对于共享的内存数据,可以采用类似 MESI 协议的机制来保证缓存一致性,减少数据同步开销,提高计算效率。
    • 任务调度与数据一致性协同:在并行计算中,任务调度需要考虑数据的一致性状态。MESI 协议提供的缓存行状态信息可以为任务调度提供参考。例如,当一个任务需要修改某个共享数据时,调度器可以根据该数据所在缓存行的 MESI 状态,合理安排任务执行顺序,避免不必要的缓存一致性开销,提高整个并行计算系统的性能。





2、并发编程的基础---CPU多级缓存-乱序执行优化

image






一、乱序执行优化的原理
  1. 指令独立性判断
    • 处理器在执行指令时,会先分析指令之间的依赖关系。对于没有数据依赖和控制依赖的指令,即它们的执行结果不会影响彼此,处理器可以打破程序代码编写的顺序来执行。例如,在图中 “a = 10” 和 “b = 200” 这两条指令,它们之间不存在数据依赖(即一条指令的操作数不是另一条指令的运算结果 ),也不存在控制依赖(比如条件判断等决定指令执行顺序的情况 )。所以处理器可以对它们的执行顺序进行重新安排,不一定按照代码中先写 “a = 10” 后写 “b = 200” 的顺序执行。
    • 处理器通过硬件逻辑,如指令调度器,来识别这些独立指令。指令调度器会跟踪指令的操作数来源和目的,以及指令之间的逻辑关系,当发现有可并行执行的独立指令时,就会将它们分配到不同的执行单元同时执行,从而提高整体执行效率。
  2. 指令重排
    • 当处理器判断出指令之间的依赖关系后,会将独立的指令进行重新排序,以充分利用处理器的资源。比如,处理器内部可能有多个执行单元,分别负责算术运算、逻辑运算、数据存储等不同功能。如果将可以并行执行的指令合理分配到这些执行单元中,就能在同一时间内执行更多的指令。例如,可能先执行 “b = 200” 这条指令,同时在另一个执行单元执行 “a = 10”,而不是依次顺序执行,这样可以有效减少指令执行的总时间。
    • 重排后的指令执行结果需要保证和按顺序执行的结果一致,这是乱序执行的一个重要原则。处理器通过寄存器重命名等技术来实现这一点。寄存器重命名会为指令操作数分配临时的寄存器,使得指令在乱序执行过程中,能够正确处理数据的流向,避免数据冲突,确保最终结果的正确性。
二、乱序执行对性能的提升
  1. 减少处理器空闲时间
    • 在传统的顺序执行方式下,当一条指令需要等待数据从内存加载或者前一条指令的运算结果时,处理器的其他执行单元可能会处于空闲状态。而乱序执行优化可以让处理器在等待这些资源的过程中,执行其他独立的指令。例如,如果 “a = 10” 这条指令需要从内存读取数据来初始化变量 a,在等待内存读取的时间里,处理器可以先执行 “b = 200” 这条不依赖内存读取结果的指令,从而充分利用处理器的计算资源,减少整体的执行时间。
    • 据测试,在一些复杂的计算密集型程序中,通过乱序执行优化,处理器的空闲时间可以降低 30% - 50%,大大提高了处理器的利用率,进而提升了程序的运行速度。
  2. 适应现代处理器架构
    • 现代多核处理器和超标量处理器架构都具备多个执行单元和复杂的缓存层次结构。乱序执行优化能够更好地匹配这种架构特点,充分发挥多核和多执行单元的优势。例如,在多核处理器中,不同核心可以同时执行不同的指令流,而每个核心内部又可以通过乱序执行对自身的指令进行优化调度。在超标量处理器中,一次可以发射多条指令,乱序执行可以让这些指令在不同执行单元中高效执行,充分利用处理器的并行处理能力。研究表明,对于现代高性能处理器,乱序执行优化可以使整体性能提升 20% - 40%。
三、乱序执行带来的问题及应对措施
  1. 数据一致性问题
    • 乱序执行可能会导致数据一致性问题,尤其是在多线程或多处理器环境下。例如,一个线程中乱序执行的指令可能会让其他线程看到不一致的数据状态。假设线程 1 执行 “a = 10” 和 “b = 200” 两条指令,并且乱序执行,此时线程 2 如果要读取 a 和 b 的值进行计算,可能会因为指令的乱序执行而读取到错误的中间状态数据,导致计算结果错误。
    • 为了解决这个问题,处理器引入了内存屏障(Memory Barrier)机制。内存屏障指令可以强制处理器按照特定的顺序执行内存访问操作,确保在某个内存屏障之前的所有写操作都完成后,才进行该内存屏障之后的读操作。在编程语言层面,也有相应的同步原语(如 Java 中的volatile关键字)来保证数据的可见性和一致性,其底层原理就和处理器的内存屏障机制相关。
  2. 程序调试困难
    • 乱序执行使得程序的执行顺序不再和代码编写顺序一致,这给程序调试带来了很大困难。当出现程序错误时,开发人员很难根据代码的顺序来分析问题,因为实际执行顺序可能已经被打乱。例如,在调试过程中,开发人员设置断点跟踪变量的值,可能会发现变量的值变化不符合预期,这是因为指令的乱序执行导致变量的赋值和使用顺序发生了改变。
    • 应对方法包括使用支持乱序执行调试的调试工具,这些工具可以记录指令的实际执行顺序和数据的变化情况,帮助开发人员分析问题。同时,开发人员在编写代码时也需要更加谨慎,遵循一定的编程规范,避免编写依赖特定指令执行顺序的代码,以减少乱序执行带来的调试难题。





3:java  内存模型


image


一、Java 内存模型(JMM)的基本结构
  1. 线程栈(Thread Stack)
    • 作用与内容:每个 Java 线程都有自己独立的线程栈,它用于存储该线程执行方法时的局部变量、方法调用的上下文等信息。如图中所示,线程栈里包含了methodOne()methodTwo()等方法,每个方法又有各自的局部变量,像Local variable 1Local variable 2。这些局部变量仅在当前方法的执行范围内有效,并且对其他线程是不可见的。
    • 生命周期:线程栈随着线程的创建而创建,随着线程的结束而销毁。当一个方法被调用时,会在栈中压入一个栈帧,包含该方法的局部变量表、操作数栈、动态链接等信息;方法执行完毕后,对应的栈帧从栈中弹出。
  2. 堆(Heap)
    • 作用与内容:堆是 Java 中用于存储对象实例的区域,所有通过new关键字创建的对象都存放在堆中,如图里的Object 1Object 5。堆是被所有线程共享的内存区域,不同线程可以访问和操作堆中的对象。
    • 内存管理:Java 的垃圾回收机制主要作用于堆,它会自动检测堆中不再被引用的对象,并回收这些对象占用的内存空间,以防止内存泄漏和提高内存利用率。
  3. 主内存(Main Memory)
    • 与硬件的关系:主内存对应于计算机硬件中的物理内存,它是 Java 虚拟机和操作系统交互的内存区域。在多处理器系统中,每个 CPU 都有自己的高速缓存(如CPU 1 cacheCPU 2 cache ),主内存是这些缓存与 Java 程序进行数据交互的中介。
    • 数据交互:Java 线程对共享变量的操作,需要通过主内存来进行。线程从主内存读取共享变量的值到自己的工作内存(对应于 CPU 缓存 )进行操作,操作完成后再将结果写回主内存。
二、JMM 中的数据交互与可见性问题
  1. 工作内存与主内存交互
    • 机制:Java 线程在执行过程中,会将主内存中的共享变量复制一份到自己的工作内存(即 CPU 缓存 )中进行操作。例如,线程 1 要操作堆中的某个共享对象,它会先将该对象相关的数据从主内存加载到CPU 1 cache中,然后在缓存中进行读写操作。操作完成后,再将修改后的数据写回主内存。
    • 指令实现:这种交互通过一系列的内存操作指令来实现,包括lock(锁定变量所在缓存行,使其独占 )、unlock(解锁 )、read(从主内存读取 )、load(将读取的值放入工作内存 )、use(使用工作内存中的值 )、assign(给工作内存中的变量赋值 )、store(将工作内存中的值传送到主内存 )、write(将值写入主内存 )等。
  2. 可见性问题
    • 问题描述:由于不同线程的工作内存是独立的,可能会出现一个线程对共享变量的修改,其他线程不能及时看到的情况。比如线程 1 修改了工作内存中的共享变量值并写回主内存,但此时线程 2 的工作内存中该变量还是旧值,就会导致数据不一致。
    • 解决方案:JMM 通过volatile关键字、synchronized关键字以及Lock接口等机制来保证共享变量的可见性。volatile关键字会强制线程每次读取变量时都从主内存获取最新值,写入时也立即写回主内存;synchronizedLock则通过互斥访问,保证同一时刻只有一个线程能访问共享资源,从而避免可见性问题。
三、JMM 与多线程编程的关系
  1. 原子性保证
    • 概念:原子性是指一个操作是不可分割的,在执行过程中不会被其他线程干扰。在 JMM 中,对于一些基本数据类型的读写操作(如intlong等 ),在单处理器系统中通常具有原子性,但在多处理器系统中可能不具备。
    • 实现方式:对于需要保证原子性的复合操作,JMM 提供了Atomic类(如AtomicIntegerAtomicLong等 ),它们通过底层的硬件指令(如 CAS,Compare - And - Swap )来实现原子操作,以及synchronized关键字和Lock接口,通过互斥锁的方式保证操作的原子性。
  2. 有序性保证
    • 问题背景:由于处理器的乱序执行优化和指令重排等原因,程序代码的执行顺序可能与编写顺序不一致,这在多线程环境下可能导致错误。
    • 解决方案:JMM 通过volatile关键字禁止指令重排,保证变量的读写操作按照代码编写的顺序执行;同时,synchronizedLock也能保证在锁的作用范围内,指令的执行顺序是有序的,因为同一时刻只有一个线程能进入同步代码块或获取锁进行操作。


image


一、各组件功能及交互
  1. CPU 寄存器(CPU Registers)
    • 功能:CPU 寄存器是 CPU 内部的高速存储单元,用于临时存储 CPU 在运算过程中的操作数、运算结果等数据。它的访问速度极快,几乎能与 CPU 的运算速度相匹配。例如,在执行算术运算指令时,操作数会先被加载到寄存器中,CPU 直接从寄存器中读取数据进行运算,运算结果也会暂存于寄存器,方便后续操作。
    • 与其他组件交互:CPU 寄存器与 CPU Cache Memory 紧密协作。当 CPU 需要数据时,会优先从寄存器中查找,若寄存器中没有所需数据,就会从 CPU Cache Memory 中读取,并将常用数据加载到寄存器中。同时,寄存器中的数据也会根据指令执行情况,适时写回到 CPU Cache Memory。
  2. CPU 缓存(CPU Cache Memory)
    • 功能:作为介于 CPU 和主存之间的高速缓冲存储器,CPU Cache Memory 用于存储 CPU 近期可能会频繁访问的数据和指令。它的速度比主存快很多,但容量相对较小。其存在是为了弥补 CPU 运算速度和主存读写速度之间的差距,减少 CPU 等待数据从主存传输的时间,提高整体运算效率。
    • 与其他组件交互:一方面,它从主存中预取数据和指令,当 CPU 需要时快速提供;另一方面,CPU 对数据的写操作也会先写入 Cache,在合适的时机(如 Cache 行被替换时 )再写回主存。同时,CPU Cache Memory 与 CPU 寄存器频繁交互,满足 CPU 对数据的快速存取需求。
  3. 主存(RAM - Main Memory)
    • 功能:主存即随机存取存储器,用于长期存储计算机正在运行的程序和数据。它是计算机系统中重要的存储部件,所有的程序运行时都需要将代码和数据加载到主存中。例如,Java 程序在运行时,类文件、对象实例等数据都存储在主存中。
    • 与其他组件交互:主存是 CPU Cache Memory 的数据来源和最终存储地。当 CPU Cache Memory 需要新数据时,会从主存读取;CPU Cache Memory 中被修改且需要持久化的数据也会写回主存。主存与 CPU Cache Memory 之间的数据传输通常以缓存行(Cache Line )为单位进行。
二、Java 内存模型(JMM)与硬件内存架构的关联
  1. 工作内存与硬件缓存对应
    • 在 JMM 中,线程的工作内存类似于硬件架构中的 CPU Cache Memory。Java 线程在执行过程中,会将主存中的共享变量复制到自己的工作内存进行操作,这就如同 CPU 从主存将数据加载到 Cache 中进行运算。例如,多个线程同时访问主存中的共享对象,每个线程会在自己的工作内存(对应 CPU Cache )中维护该对象的副本,对副本进行读写操作。
  2. 主内存映射
    • JMM 中的主内存对应于硬件架构中的主存(RAM - Main Memory )。Java 程序中所有线程共享的对象和变量都存储在主内存中,就像计算机系统中所有程序运行所需的数据和代码都存储在主存一样。线程对共享变量的操作结果最终要写回到主内存,保证数据在多线程间的一致性,这类似于硬件层面上 CPU Cache 将修改后的数据写回主存。
  3. 指令重排与缓存一致性
    • 硬件层面的指令重排和缓存一致性问题在 JMM 中也有体现。处理器为提高性能可能会对指令进行重排,这可能影响程序执行结果的正确性。JMM 通过volatile等关键字和同步机制来禁止指令重排,保证有序性;同时,JMM 的可见性机制(如volatile强制读写主存 )也类似于硬件层面的缓存一致性协议(如 MESI ),确保多线程环境下共享变量的可见性和一致性。




一、硬件组件的深度剖析
  1. CPU 寄存器(CPU Registers)
    • 分类及用途:CPU 寄存器可细分为多种类型。通用寄存器用于存储一般性的数据和地址,像 x86 架构下的 EAX、EBX 等寄存器,能在算术运算、逻辑运算等操作中暂存操作数和结果;专用寄存器则承担特定功能,如指令指针寄存器(IP,在 x86 架构中 )用于存储下一条要执行的指令地址,控制着程序执行流程;标志寄存器用于记录运算结果的特征,如是否产生进位、溢出,运算结果是否为零等状态信息,这些标志会影响后续指令的执行逻辑。
    • 高速特性的实现原理:CPU 寄存器通常由高速的静态随机存取存储器(SRAM)构成。SRAM 不需要像动态随机存取存储器(DRAM)那样周期性刷新,其内部电路结构简单且晶体管数量相对较少,使得信号传输延迟极小,能在极短时间内完成数据的读写操作,从而与 CPU 的高速运算相匹配,满足 CPU 对数据快速存取的要求。
  2. CPU 缓存(CPU Cache Memory)
    • 多级缓存结构:现代 CPU 通常采用多级缓存结构,一般分为 L1、L2 和 L3 缓存。L1 缓存速度最快,但容量最小,通常集成在 CPU 核心内部,分为指令缓存(存放指令 )和数据缓存(存放数据 ),能让 CPU 快速获取指令和数据,减少等待时间。L2 缓存容量稍大,速度略慢于 L1 缓存,可为多个 CPU 核心共享或部分共享。L3 缓存容量更大,速度相对更慢一些,主要作用是进一步减少 CPU 访问主存的次数,提升整体性能。例如,英特尔酷睿系列处理器的 L1 缓存容量一般在几十 KB,L2 缓存可达几百 KB,L3 缓存则能达到几 MB 甚至更高。
    • 缓存行(Cache Line)机制:CPU Cache 以缓存行作为数据传输的基本单位,一般缓存行大小为 32 字节、64 字节或 128 字节等。当 CPU 需要读取数据时,会以缓存行为单位从主存中读取数据到 Cache 中。例如,如果 CPU 要读取一个变量的值,即使该变量只占几个字节,也会将包含该变量的整个缓存行数据读取到 Cache。这样做是因为程序具有局部性原理,即一段时间内程序往往会集中访问某些特定区域的数据和指令,预取整个缓存行可以提高后续数据访问命中 Cache 的概率。
    • 缓存一致性协议:在多核 CPU 系统中,为保证多个 CPU Cache 之间数据的一致性,采用了如 MESI 协议等机制。MESI 协议定义了缓存行的四种状态(M:已修改、E:独占、S:共享、I:无效 ),通过状态转换规则和总线嗅探等机制,确保在多个 CPU 核心同时访问共享数据时,各个 Cache 中的数据与主存数据保持一致。例如,当一个 CPU 核心修改了处于共享状态的缓存行数据时,会通过总线通知其他 CPU 核心将其对应的缓存行状态置为无效,保证数据一致性。
  3. 主存(RAM - Main Memory)
    • 动态随机存取存储器(DRAM)原理:主存一般由 DRAM 构成。DRAM 通过电容存储电荷来表示数据,一个电容存储一位数据,充电表示 1,未充电表示 0 。但由于电容存在漏电现象,电荷会逐渐丢失,所以需要周期性地对电容进行刷新操作来维持数据。这种刷新机制使得 DRAM 的读写速度相对较慢,不过其优势在于单位容量成本较低,可以实现较大的存储容量,满足计算机系统对数据和程序存储的需求。
    • 内存控制器:主存通过内存控制器与 CPU 及其他组件进行交互。内存控制器负责管理主存的读写操作,它可以对来自 CPU 的内存访问请求进行仲裁、调度和转换。例如,当多个 CPU 核心同时请求访问主存时,内存控制器会根据一定的算法(如轮询、优先级等 )来决定访问顺序,确保内存访问的高效性和有序性。同时,内存控制器还负责将 CPU 发出的地址信号和数据信号转换为主存能够识别的格式,实现两者之间的通信。
二、Java 内存模型(JMM)与硬件内存架构的深入关联
  1. 工作内存的缓存模拟及差异
    • 模拟机制:JMM 中线程的工作内存模拟了硬件 CPU Cache 的功能。Java 线程在执行时,将主存中的共享变量复制到工作内存进行操作,类似于 CPU 从主存读取数据到 Cache 进行运算。比如在多线程环境下,多个线程对主存中同一共享对象的操作,会在各自工作内存中维护对象副本并操作,就像多个 CPU 核心在各自 Cache 中处理共享数据副本。
    • 差异:然而,工作内存与 CPU Cache 存在不同。工作内存是 JMM 从软件层面抽象出来的概念,并非实际的物理存储单元;而 CPU Cache 是实实在在的硬件组件。并且,工作内存对共享变量的操作规则是由 JMM 定义的,如通过volatilesynchronized等机制保证可见性、原子性和有序性,这与硬件层面通过缓存一致性协议保证数据一致性的方式在实现和原理上有差异。
  2. 主内存与硬件主存的一致性保障
    • 数据同步:JMM 中的主内存对应硬件主存,Java 程序中共享变量存储于主内存,如同硬件中程序数据存于主存。但在数据同步方面,JMM 通过一系列规则和关键字来确保线程间数据一致性。例如,volatile关键字强制线程每次读取变量从主内存获取最新值,写入时立即写回主内存,这类似于硬件层面通过缓存一致性协议保证 Cache 与主存的数据同步,但实现细节和触发机制不同。
    • 内存屏障:JMM 引入内存屏障指令来保证指令执行顺序和数据可见性,这与硬件层面的内存屏障概念有相似之处。硬件内存屏障用于强制特定顺序的内存访问操作,确保 CPU Cache 与主存间数据操作的有序性和一致性;JMM 中的内存屏障则是在 Java 层面通过字节码指令实现,作用于多线程环境下对共享变量的操作,保证线程间的有序性和可见性。
  3. 指令重排的软硬件协同与差异
    • 协同:硬件层面的指令重排是处理器为提高性能,在不改变程序最终执行结果的前提下,对指令执行顺序进行重新排列。JMM 中的指令重排也基于类似原理,允许在不影响单线程程序正确性的情况下对指令进行重排。两者都要考虑数据依赖关系,确保有数据依赖的指令按正确顺序执行。例如,对于 “a = 1; b = a + 1;” 这样有数据依赖的代码,无论是硬件还是 JMM 层面都不会重排这两条指令的执行顺序。
    • 差异:硬件指令重排由 CPU 内部的硬件逻辑(如指令调度器 )完成,主要基于硬件架构特性和性能优化需求;JMM 的指令重排规则是在 Java 语言规范中定义的,是从语言层面为了提高程序执行效率而做出的规定,并且通过volatile等关键字和同步机制来限制和规范指令重排,以保证多线程程序的正确性,这与硬件层面通过缓存一致性协议等硬件机制来保证数据一致性和有序性的方式不同。



image






一、Java 虚拟机(JVM)组件剖析
  1. 线程栈(Thread Stack)
    • 功能原理:每个 Java 线程都拥有独立的线程栈,它是线程执行方法时的运行时数据区。当线程调用一个方法,会在栈中压入一个栈帧。栈帧包含局部变量表、操作数栈、动态链接等信息。局部变量表用于存储方法参数和局部变量,操作数栈用于执行运算等操作时暂存操作数,动态链接则负责将符号引用转换为直接引用。例如,一个方法接收一个整数参数并定义一个局部变量进行计算,这些数据都存储在线程栈的对应栈帧的局部变量表中,计算过程中的临时数据存于操作数栈。
    • 生命周期:线程栈随线程创建而生成,随线程结束而销毁。当方法调用开始,栈帧入栈;方法执行完毕,栈帧出栈。若线程执行过程中栈深度超过虚拟机允许的最大深度(可通过-Xss参数设置 ),会抛出StackOverflowError异常。
  2. 堆(Heap)
    • 存储机制:堆是 Java 中用于存储对象实例的区域,是所有线程共享的内存空间。几乎所有通过new关键字创建的对象都在堆上分配内存。堆内存大小可通过-Xmx(最大堆大小 )和-Xms(初始堆大小 )参数设置。例如创建一个User对象User user = new User();,该对象的实例数据就存放在堆中。
    • 垃圾回收:Java 的垃圾回收机制(GC)主要作用于堆。GC 会自动检测堆中不再被引用的对象,通过标记 - 清除、标记 - 整理、复制等算法回收这些对象占用的内存空间。不同的垃圾回收器(如 Serial GC、Parallel GC、CMS GC、G1 GC 等 )在回收策略和性能上有所差异,以适应不同应用场景对内存回收的需求。
二、硬件内存架构组件剖析
  1. CPU 寄存器(CPU Registers)
    • 工作机制:CPU 寄存器是 CPU 内部的高速存储单元,用于临时存储 CPU 在运算过程中的操作数、运算结果等数据。它的访问速度极快,能与 CPU 的运算速度相匹配。例如,在执行算术运算指令时,操作数会先被加载到寄存器中,CPU 直接从寄存器中读取数据进行运算,运算结果也会暂存于寄存器,方便后续操作。
    • 分类与用途:CPU 寄存器可分为通用寄存器(如 x86 架构下的 EAX、EBX 等,用于存储一般性的数据和地址 )、专用寄存器(如指令指针寄存器 IP,用于存储下一条要执行的指令地址 )、标志寄存器(用于记录运算结果的特征,如是否产生进位、溢出等 )。不同类型的寄存器在 CPU 执行指令过程中各司其职,共同保障指令的高效执行。
  2. CPU 缓存(CPU Cache Memory)
    • 多级缓存结构:现代 CPU 通常采用多级缓存结构,一般分为 L1、L2 和 L3 缓存。L1 缓存速度最快,但容量最小,通常集成在 CPU 核心内部,分为指令缓存和数据缓存,能让 CPU 快速获取指令和数据,减少等待时间。L2 缓存容量稍大,速度略慢于 L1 缓存,可为多个 CPU 核心共享或部分共享。L3 缓存容量更大,速度相对更慢一些,主要作用是进一步减少 CPU 访问主存的次数,提升整体性能。
    • 缓存行机制:CPU Cache 以缓存行作为数据传输的基本单位,一般缓存行大小为 32 字节、64 字节或 128 字节等。当 CPU 需要读取数据时,会以缓存行为单位从主存中读取数据到 Cache 中。这是基于程序的局部性原理,即一段时间内程序往往会集中访问某些特定区域的数据和指令,预取整个缓存行可以提高后续数据访问命中 Cache 的概率。
  3. 主存(RAM - Main Memory)
    • 存储原理:主存即随机存取存储器,用于长期存储计算机正在运行的程序和数据。它由动态随机存取存储器(DRAM)构成,通过电容存储电荷来表示数据,一个电容存储一位数据,充电表示 1,未充电表示 0 。但由于电容存在漏电现象,电荷会逐渐丢失,所以需要周期性地对电容进行刷新操作来维持数据。
    • 内存管理:主存通过内存控制器与 CPU 及其他组件进行交互。内存控制器负责管理主存的读写操作,对来自 CPU 的内存访问请求进行仲裁、调度和转换。当多个 CPU 核心同时请求访问主存时,内存控制器会根据一定的算法(如轮询、优先级等 )来决定访问顺序,确保内存访问的高效性和有序性。
三、JMM 与硬件内存架构的关联
  1. 数据交互层面
    • 线程栈与 CPU 寄存器:线程栈中的局部变量在方法执行时,部分数据可能会被加载到 CPU 寄存器中进行运算,因为寄存器的高速访问特性可以加快运算速度。例如,一个简单的整数加法运算,局部变量表中的操作数可能会被加载到通用寄存器中进行加法运算。
    • 堆与主存:堆中的对象数据存储在主存中,Java 线程对堆中对象的操作,需要通过主存来进行数据交互。线程从主存读取对象数据到自己的工作内存(类似于 CPU Cache )进行操作,操作完成后再将结果写回主存。
  2. 缓存与一致性层面
    • 工作内存与 CPU Cache:JMM 中线程的工作内存类似于硬件架构中的 CPU Cache Memory。Java 线程在执行过程中,会将主存中的共享变量复制一份到自己的工作内存进行操作,类似于 CPU 从主存读取数据到 Cache 进行运算。为保证数据一致性,JMM 通过volatile等关键字和同步机制来处理,这与硬件层面的缓存一致性协议(如 MESI )作用类似,但实现方式不同。
    • 内存屏障关联:JMM 引入内存屏障指令来保证指令执行顺序和数据可见性,硬件层面也有内存屏障用于强制特定顺序的内存访问操作,确保 CPU Cache 与主存间数据操作的有序性和一致性。两者在保证数据一致性和有序性上的目的相同,但实现细节和触发机制存在差异。


差异
  1. 本质与功能
    • Java 虚拟机:是软件层面的抽象概念,可看成一个虚拟的计算机系统 。它屏蔽了底层硬件和操作系统差异,为 Java 程序提供统一运行环境。通过加载、验证、准备、解析、初始化等步骤处理字节码文件,借助指令集、寄存器、栈、堆、方法区域等组件协同工作,让 Java 程序能跨平台运行。比如在 Windows 系统上编写的 Java 程序,在安装对应 JVM 的 Linux 系统也能运行。
    • CPU 架构:属于硬件层面,是 CPU 内部的物理结构设计,包含运算器、控制器、寄存器组等硬件组件。不同 CPU 架构(如 x86、ARM )指令集、性能、功耗等特性不同,直接决定计算机运算、处理数据能力,与具体硬件电路和物理实现紧密相关。像 x86 架构常用于 PC,ARM 架构在移动设备和嵌入式系统广泛应用。
  2. 指令集
    • Java 虚拟机:拥有一套自己的字节码指令集 ,指令操作基于 Java 虚拟机栈进行。字节码指令是对 Java 源代码的一种中间表示形式,与具体硬件指令集无直接映射,具有平台无关性。例如iadd指令用于整数相加操作,不管底层是何种 CPU,在 JVM 中执行逻辑相同。
    • CPU 架构:不同 CPU 架构有各自的指令集,像 CISC(复杂指令集计算机,如 x86 )指令集复杂,单条指令可执行多个低级操作;RISC(精简指令集计算机,如 ARM )指令集简单,由简单指令组合完成复杂操作。且 CPU 指令集与硬件电路紧密相关,不同 CPU 架构指令集互不兼容。
  3. 内存管理
    • Java 虚拟机:自动管理内存,通过垃圾回收机制回收不再使用的对象所占用内存,开发人员无需手动释放。JVM 内存分堆、方法区、虚拟机栈、本地方法栈、程序计数器等区域,不同区域有不同作用和生命周期。比如堆用于存储对象实例,方法区存放类信息、常量等。
    • CPU 架构:本身不直接管理内存,通过内存管理单元(MMU)与操作系统配合管理内存。CPU 访问内存需经过 MMU 转换虚拟地址为物理地址,且不同 CPU 架构对内存的访问权限、缓存机制等处理方式不同。例如在具有多级缓存的 CPU 中,数据在缓存和主存间传输由硬件缓存一致性协议管理。
  4. 执行效率
    • Java 虚拟机:执行 Java 程序时,字节码需先解释或即时编译成机器码,相比直接在 CPU 上执行机器码,多了转换过程,可能带来一定性能损耗。但现代 JVM 通过即时编译技术、优化算法等不断提升执行效率。
    • CPU 架构:直接执行机器码,理论上执行效率高。不过实际效率受指令集复杂度、缓存大小与性能、流水线设计等多种硬件因素影响。例如采用先进流水线设计和大容量高速缓存的 CPU,指令执行效率更高。
联系
  1. 运行基础:Java 虚拟机运行依赖于 CPU 架构提供的底层运算和控制能力。CPU 的运算器执行 JVM 字节码指令中涉及的算术、逻辑运算等操作;控制器负责协调指令读取、译码和执行流程。没有 CPU 的运算和控制功能,JVM 无法完成字节码执行等操作。
  2. 指令转换:JVM 字节码指令最终需转换为对应 CPU 架构的机器码才能执行。解释执行方式下,JVM 逐行将字节码解释成 CPU 机器码并执行;即时编译(JIT)方式下,JVM 在运行时将热点代码编译成机器码缓存起来供后续快速执行,这都基于 CPU 架构的指令集特性进行转换。
  3. 资源映射:JVM 中一些概念和组件与 CPU 架构存在映射关系。如 JVM 的程序计数器类似于 CPU 的指令指针寄存器,用于记录下一条要执行的指令地址;JVM 工作内存中对共享变量的操作与 CPU 缓存中数据读写类似,都需考虑数据一致性问题。
  4. 性能关联:CPU 架构性能(如运算速度、缓存大小、指令执行效率等 )直接影响 JVM 运行效率。高性能 CPU 架构能为 JVM 提供更快速运算和数据访问能力;同时,JVM 通过自身优化(如即时编译、垃圾回收算法优化 )也能更好利用 CPU 资源,提升整体性能。


image



一、Java 内存模型(JMM)基本概念

Java 内存模型(JMM)是 Java 虚拟机规范中定义的一种抽象概念,用于屏蔽各种硬件和操作系统的内存访问差异,以实现 Java 程序在不同平台下都能达到一致的内存访问效果。它规定了 Java 程序中各个变量(包括实例字段、静态字段和构成数组对象的元素 )的访问规则,以及在多线程环境下如何保证数据的可见性、原子性和有序性。

二、图中各部分详解
  1. 线程 A 和线程 B
    • 线程的定义与作用:线程是程序执行的最小单元,是进程内的一个独立执行路径。在 Java 中,多线程允许程序同时执行多个任务,提高程序的执行效率和资源利用率。例如,一个网络应用程序中,可能有一个线程负责接收网络数据,另一个线程负责处理接收到的数据,从而实现数据的高效处理。
    • 多线程的并发问题:在多线程环境下,由于多个线程可能同时访问和操作共享变量,会出现数据竞争、线程安全等问题。比如两个线程同时对一个共享的计数器变量进行自增操作,可能会导致最终结果与预期不符。
  2. 本地内存 A 和本地内存 B
    • 本地内存的概念:本地内存是 JMM 为每个线程抽象出来的概念,它类似于硬件层面的 CPU 缓存。每个线程在执行过程中,会将主存中的共享变量复制一份到自己的本地内存中进行操作。这样做的目的是为了提高线程的执行效率,减少对主存的频繁访问。
    • 本地内存与共享变量副本:本地内存中存储的是共享变量的副本。线程对共享变量的读写操作,实际上是在本地内存中的副本上进行的。例如,线程 A 要读取主存中的共享变量count,它会先将count从主存复制到本地内存 A 中,然后对本地内存 A 中的count副本进行读取操作。
  3. 主内存
    • 主内存的作用:主内存是所有线程共享的内存区域,它对应于硬件层面的物理内存。在 Java 中,所有的共享变量都存储在主内存中。主内存是线程之间进行数据交互的中介,线程对共享变量的修改最终要写回到主内存,其他线程从主内存中读取共享变量的最新值。
    • 数据交互过程:当线程 A 对本地内存 A 中的共享变量副本进行修改后,需要将修改后的值写回到主内存中,才能被其他线程(如线程 B )看到。同样,线程 B 在读取共享变量时,也需要从主内存中获取最新的值。
  4. JMM 控制
    • JMM 的控制机制:JMM 通过一系列的规则和机制来控制线程、本地内存和主内存之间的数据交互,以保证多线程环境下数据的一致性。例如,JMM 通过volatile关键字、synchronized关键字以及Lock接口等机制来实现数据的可见性、原子性和有序性。
    • 具体控制方式volatile关键字可以保证共享变量的可见性,当一个线程修改了volatile修饰的变量后,会立即将修改后的值写回主内存,并且其他线程在读取该变量时,会强制从主内存中获取最新值。synchronized关键字和Lock接口则通过互斥访问的方式,保证同一时刻只有一个线程能访问共享资源,从而避免数据竞争问题。
三、JMM 解决的问题
  1. 可见性问题:由于线程对共享变量的操作是在本地内存中进行的,可能会出现一个线程对共享变量的修改,其他线程不能及时看到的情况。JMM 通过volatile关键字、synchronized关键字以及Lock接口等机制来保证共享变量的可见性,确保一个线程对共享变量的修改能够及时被其他线程看到。
  2. 原子性问题:在多线程环境下,对于一些复合操作(如i++ ),如果不进行原子性保证,可能会出现操作被打断的情况,导致结果错误。JMM 提供了Atomic类(如AtomicIntegerAtomicLong等 ),它们通过底层的硬件指令(如 CAS,Compare - And - Swap )来实现原子操作,以及synchronized关键字和Lock接口,通过互斥锁的方式保证操作的原子性。
  3. 有序性问题:由于处理器的乱序执行优化和指令重排等原因,程序代码的执行顺序可能与编写顺序不一致,这在多线程环境下可能导致错误。JMM 通过volatile关键字禁止指令重排,保证变量的读写操作按照代码编写的顺序执行;同时,synchronizedLock也能保证在锁的作用范围内,指令的执行顺序是有序的。




数据通信方式
  1. 基于原子操作:Java 内存模型(JMM)定义了一系列原子操作来规范 Java 虚拟机内存(线程工作内存)与主内存间的数据通信。包括read(从主存读取变量值到工作内存)、load(将read获取的值放入工作内存变量副本)、use(将工作内存变量值传递给执行引擎)、assign(将执行引擎的值赋给工作内存变量)、store(把工作内存变量值传至主存)、write(将store的值写入主存变量)、lock(锁定主存变量供线程独占)、unlock(释放锁定变量) 。这些操作确保数据在不同内存区域间有序、准确传输。
  2. 同步机制辅助synchronized关键字、Lock接口等同步机制在数据通信中起重要作用。以synchronized为例,线程进入同步代码块时,会先从主内存刷新共享变量到工作内存,保证读取到最新值;退出时,会将工作内存中修改后的变量值写回主内存,确保其他线程能看到更新。
通信途径
  1. CPU 缓存中介:实际通信常借助 CPU 缓存。当线程从主内存读取共享变量,数据先载入 CPU 缓存,再到工作内存;写回时,可能先存于 CPU 缓存,合适时机再回写主内存。这利用了 CPU 缓存高速特性,减少主存直接访问次数,提升通信效率。
  2. 系统总线传输:系统总线作为硬件连接线路,承载数据在 Java 虚拟机内存与主内存间的传输任务。无论是从主存读取变量值,还是向主存写入值,都经系统总线完成物理层面的数据移动。
通信内容
  1. 共享变量值:主要是对象实例字段、静态字段及数组元素等共享变量的值。如多线程操作同一User对象的age字段,该字段值会在主内存和各线程工作内存间传输。线程读取时从主存获取,修改后写回主存供其他线程读取新值。
  2. 对象引用:工作内存存储对象引用,借此从主内存定位对象数据。线程操作对象属性,先依引用从主内存取数据到工作内存,操作后写回,保证对象状态在多线程间一致。
通信差异
  1. 抽象与物理差异:Java 虚拟机内存与主内存通信基于 JMM 抽象模型,屏蔽硬件和操作系统差异,通过定义规则和原子操作实现。硬件层面内存通信基于具体硬件架构(如 CPU、缓存、主存物理连接)和缓存一致性协议(如 MESI ),是物理组件直接交互。
  2. 实现复杂度差异:JVM 通信实现依赖 Java 运行时环境,围绕原子操作和同步机制,由软件层面的 JVM 规范和代码实现。硬件通信涉及缓存控制器、总线仲裁器等硬件电路设计及复杂信号处理逻辑,实现复杂度高且与硬件制造工艺紧密相关。
  3. 目的侧重差异:JVM 通信着重保证多线程程序中共享变量访问正确性,实现数据可见性、原子性和有序性,确保程序逻辑正确。硬件通信主要为提升 CPU 访问内存效率,减少等待时间,虽也有数据一致性保障需求,但出发点和侧重点不同。


数据通信方式

Java 虚拟机内存与主内存之间通过一系列原子操作来进行数据通信 ,这些操作由 Java 内存模型(JMM)定义,主要包括:

  • read(读取):作用于主内存变量,将一个变量值从主内存传输到线程的工作内存中,为后续的 load 动作提供数据。比如,线程要操作主内存中的变量num,首先通过read操作把num的值传输到工作内存。
  • load(载入):作用于工作内存的变量,它将read操作从主内存中得到的变量值放入工作内存的变量副本中。接上例,在read操作后,通过load操作将num的值放入工作内存的变量副本,这样线程就能在工作内存中对num进行操作。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。当虚拟机执行到需要使用变量值的字节码指令时,就会执行该操作。例如执行计算num + 5的指令时,会通过use操作将工作内存中num的值传递给执行引擎参与运算。
  • assign(赋值):作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量。若执行num = 10这样的赋值指令,虚拟机就会通过assign操作将10这个值赋给工作内存中的num变量副本。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,为后续的write操作做准备。比如线程对工作内存中的num变量副本完成操作后,通过store操作将其值传送到主内存。
  • write(写入):作用于主内存的变量,将store操作从工作内存中得到的变量的值放入主内存的变量中。即把store操作传输过来的值真正写入主内存的变量中,完成工作内存到主内存的数据同步。
  • lock(锁定):作用于主内存变量,把该变量标记为一个线程独占状态。在多线程对共享变量进行操作时,通过lock操作可以保证在某一时刻只有一个线程能对变量进行操作,防止数据竞争。
  • unlock(解锁):作用于主内存变量,释放被lock操作锁定的变量,使其可以被其他线程锁定并操作 。
通信途径

Java 虚拟机内存与主内存之间的数据通信主要通过高速缓存、系统总线等硬件组件来实现:

  • 高速缓存(CPU Cache):Java 线程工作内存中的数据与主内存交互时,往往会经过 CPU Cache。由于 CPU Cache 速度快,当线程从主内存读取数据时,数据会先加载到 CPU Cache,再进入工作内存;写回数据时,也可能先写入 CPU Cache,在合适时机再写回主内存。这类似于硬件层面 CPU 与主存交互利用 Cache 提高效率,比如一个线程频繁读取主内存中某对象的属性值,该对象数据会被缓存到 CPU Cache,后续读取可直接从 Cache 获取,减少主存访问次数。
  • 系统总线:作为连接计算机各部件(如 CPU、内存、I/O 设备等)的通信线路,系统总线负责在 Java 虚拟机内存与主内存之间传输数据。线程从主内存读取变量值或向主内存写入变量值,都需要通过系统总线进行数据传输。例如,当执行readwrite操作时,数据会通过系统总线在主内存和工作内存(经过 CPU Cache 等环节 )之间传输。
通信内容

Java 虚拟机内存与主内存之间通信的内容主要是共享变量相关数据:

  • 变量值:包括实例字段、静态字段和构成数组对象的元素等。例如,在一个多线程的 Java 程序中,多个线程可能会共享一个User对象的实例字段,像user.age,那么这个age字段的值就会在 Java 虚拟机内存(线程工作内存 )与主内存之间进行传输。当一个线程修改了user.age的值,通过storewrite操作将新值写回主内存,其他线程再通过readload操作从主内存读取更新后的值。
  • 对象引用:工作内存只会拷贝对象的引用,而不是直接拷贝整个对象,对象在堆中生成且对所有线程可见。例如创建User user = new User();后,线程工作内存中存储的是user这个对象的引用,通过这个引用可以在主内存中找到实际的对象数据。当线程需要操作对象的属性时,会依据这个引用从主内存获取对象数据到工作内存进行操作,操作完成后再写回主内存。
通信差异

Java 虚拟机内存与主内存之间的通信和纯粹硬件层面的内存通信存在一些差异:

  • 抽象层次:Java 虚拟机内存与主内存之间的通信是基于 Java 内存模型的抽象概念。JMM 定义了一套规则和操作来规范数据在两者之间的交互,屏蔽了底层硬件和操作系统的内存访问差异。而硬件层面的内存通信直接涉及 CPU、缓存、主存等物理组件的交互,依赖于具体的硬件架构和缓存一致性协议(如 MESI 等 )。例如在硬件中,多个 CPU 核心的缓存与主存之间通过总线嗅探等机制维持数据一致性;在 JMM 中则通过volatile等关键字和一系列原子操作来保证多线程下数据的可见性和一致性。
  • 实现机制:在 Java 虚拟机中,通过readload等八种原子操作实现数据在工作内存和主内存间的通信。这些操作由 Java 虚拟机在运行时执行,与 Java 程序的字节码指令执行相关。而硬件层面的内存通信依赖于硬件电路、缓存控制器等物理部件的协同工作,通过硬件指令和信号传输实现数据在 CPU 寄存器、缓存和主存之间的流动。比如硬件中数据在缓存行中的传输和状态转换由硬件逻辑自动处理,与 JVM 的原子操作实现逻辑不同。
  • 目的侧重点:Java 虚拟机内存与主内存之间通信的主要目的是保证多线程环境下 Java 程序对共享变量访问的正确性,实现数据的可见性、原子性和有序性,以确保程序逻辑的正确执行。而硬件层面内存通信侧重于提高 CPU 访问内存的效率,减少 CPU 等待内存读写的时间,通过缓存等机制加速数据访问,其数据一致性保障是为了保证多个 CPU 核心对共享内存访问的正确性,侧重点略有不同。



image


image




一、lock(锁定)
  • 原理lock操作是 Java 内存模型中保证多线程对共享变量互斥访问的关键。当一个线程对主内存中的变量执行lock操作时,该变量会被标记为被当前线程独占。这就像给变量加了一把锁,在该线程持有锁期间,其他线程无法对这个变量进行操作。
  • 应用场景:常用于synchronized同步块。例如,在多线程同时操作一个银行账户余额时,为防止多个线程同时修改余额导致数据混乱,可将修改余额的代码放在synchronized块中。当一个线程进入这个块时,会对相关的余额变量执行lock操作,独占该变量,确保操作的原子性。
  • 实现机制:在底层实现上,lock操作通常依赖于 CPU 提供的原子指令,如x86架构下的LOCK前缀指令。它会在总线上发出一个锁定信号,阻止其他 CPU 核心对共享内存区域的访问,从而实现变量的独占访问。
二、unlock(解锁)
  • 原理unlock操作是与lock操作对应的。当一个线程完成对共享变量的操作后,需要通过unlock操作释放对变量的独占状态。只有执行了unlock操作,其他线程才能对该变量执行lock操作并进行后续处理。
  • 应用场景:还是以银行账户余额操作为例,当一个线程完成对余额的修改并确认无误后,它会执行unlock操作,释放对余额变量的独占。这样,其他线程就可以获取这个变量的锁,继续进行操作。
  • 与其他操作的关联unlock操作必须与lock操作成对出现。并且,在执行unlock操作之前,必须确保该线程对变量的修改已经通过storewrite操作写回到主内存中,以保证其他线程能读取到最新的值。
三、read(读取)
  • 原理read操作是线程从主内存获取共享变量值的第一步。它将主内存中变量的值传输到线程的工作内存中,为后续的load操作提供数据来源。
  • 应用场景:当线程需要读取共享变量的值进行计算、判断等操作时,就会执行read操作。比如在一个多线程计算任务中,每个线程需要读取共享的计算参数,此时就会对这些参数执行read操作。
  • 数据传输过程:在传输过程中,read操作会按照一定的规则从主内存中读取变量值。这个过程可能涉及到缓存一致性协议等底层机制,以确保读取到的是最新的值。例如,如果变量值在主内存中被修改,并且其他线程已经将修改后的值写回主内存,那么当前线程执行read操作时应该能获取到这个最新值。
四、load(载入)
  • 原理load操作是在read操作之后进行的。它将read操作从主内存中获取到的变量值放入工作内存的变量副本中。这样,线程就可以在自己的工作内存中对这个副本进行操作,而不必每次都直接访问主内存。
  • 作用:提高了线程的执行效率。因为工作内存的访问速度通常比主内存快很多,将变量值载入工作内存后,线程后续对变量的操作可以在工作内存中快速完成,减少了对主内存的频繁访问。
  • 与其他操作的协同load操作与read操作紧密配合,并且为后续的use操作提供数据。例如,在执行一个计算任务时,readload操作先将计算所需的变量值载入工作内存,然后use操作再将这个值传递给执行引擎进行计算。
五、use(使用)
  • 原理use操作将工作内存中的变量值传递给执行引擎。执行引擎是 Java 虚拟机中负责执行字节码指令的组件,use操作就是为执行引擎提供操作数,使其能够进行各种运算和指令执行。
  • 应用场景:在执行算术运算、逻辑判断等指令时,都需要use操作来提供变量值,都需要use操作来提供变量值。例如,在执行int result = a + b;这样的加法运算指令时,use操作会将工作内存中ab的值传递给执行引擎,执行引擎才能进行加法运算。
  • 与其他操作的顺序关系use操作通常在load操作之后执行,因为它依赖于工作内存中已经载入的变量值。并且,在执行use操作后,可能会根据指令的要求,继续进行assign等操作。
六、assign(赋值)
  • 原理assign操作将从执行引擎接收到的值赋值给工作内存的变量。当执行引擎完成运算或其他操作后,可能会产生一个新的值,这个值需要通过assign操作存储到工作内存的变量中。
  • 应用场景:在变量赋值语句中经常会用到。比如a = 10;,当执行引擎计算出10这个值后,assign操作会将10赋值给工作内存中的变量a
  • 数据更新过程:通过assign操作,工作内存中的变量值得到更新。这个更新后的变量值可能会在后续的操作中被再次使用,或者通过storewrite操作写回到主内存中。
七、store(存储)
  • 原理store操作把工作内存中的变量值传送到主内存中,为后续的write操作做准备。当线程对工作内存中的变量进行修改后,需要将修改后的值存储到主内存中,以保证其他线程能读取到最新的值。
  • 应用场景:在多线程共享变量的场景下,当一个线程修改了共享变量的值后,就需要执行store操作。例如,在一个多线程的计数器程序中,当一个线程对计数器变量进行自增操作后,会通过store操作将自增后的值传送到主内存。
  • 数据传输机制:在传输过程中,store操作会遵循 Java 内存模型的相关规则,确保数据能够准确地从工作内存传输到主内存。这可能涉及到与缓存一致性协议的协同,以保证数据的一致性。
八、write(写入)
  • 原理write操作是将store操作从工作内存中得到的变量值写入主内存的变量中。它是将线程对变量的修改最终持久化到主内存的关键操作。
  • 作用:保证了多线程环境下共享变量值的一致性。当一个线程执行write操作后,其他线程在执行read操作时就能获取到最新的变量值。
  • 与其他操作的关联write操作必须在store操作之后执行,并且write操作的完成也标志着一个线程对共享变量的修改在主内存中得到了更新。同时,write操作也与lockunlock操作相关联,在执行write操作之前,通常需要确保当前线程持有变量的锁,以保证数据的一致性和完整性。

image




image


image



image



image




变量在主内存与工作内存间的传输规则
  • 传输操作顺序性:将变量从主内存复制到工作内存,需按顺序执行readload操作;从工作内存同步回主内存,要依次执行storewrite操作 。这确保数据在不同内存区域间传输的有序性。例如线程读取主内存中共享变量count的值,先read获取值,再load到工作内存;修改后想写回主内存,先store准备值,再write更新主内存中的值。不过,JMM 仅要求顺序执行,不强制连续,在实际执行中,这两组操作间可能会穿插其他操作。
  • 操作配对要求readloadstorewrite操作不能单独出现,必须成对。这是为保证数据传输完整,防止只进行读取或写入的部分操作,导致数据不一致。如只readload,工作内存无法获取变量值;只storewrite,主内存无法得到更新值。
工作内存中变量操作规则
  • 变量修改同步要求:线程不允许丢弃最近的assign操作,即工作内存中变量改变后必须同步到主内存。比如线程对工作内存中变量num进行assign赋值操作后,后续需通过storewrite操作将新值同步回主内存,保证其他线程能读取到最新值,避免数据不一致。
  • 无因同步禁止:线程不能无原因(未发生assign操作 )就将数据从工作内存同步回主内存。这防止无效同步,确保每次同步都基于变量在工作内存有实际修改,提高内存操作效率和数据准确性。
  • 变量初始化要求:新变量只能在主内存诞生,工作内存不能直接使用未初始化(未loadassign )的变量。在对变量实施use(传递给执行引擎 )和store(传至主内存 )操作前,必须先执行assign(赋值 )和load(载入 )操作,保证变量有合法初始值,避免使用未初始化变量引发错误。
lockunlock操作规则
  • 互斥与重入规则:同一时刻仅允许一条线程对变量进行lock操作,保证多线程对共享变量的互斥访问。同时,lock操作可被同一条线程重复执行多次(即重入 ),但只有执行相同次数的unlock操作,变量才会被解锁,lockunlock必须成对出现。如synchronized修饰的代码块,线程多次进入(重入 ),每次进入执行lock,离开执行unlock,确保代码块内操作的原子性。
  • 变量状态与操作关联:对变量执行lock操作,会清空工作内存中此变量的值,后续执行引擎使用该变量前,需重新loadassign初始化。且未被lock锁定的变量,不允许执行unlock;也不能unlock被其他线程锁定的变量。另外,执行unlock操作前,必须先通过storewrite将变量同步到主内存,保证变量最新状态在主内存更新,维护多线程环境下数据一致性。



变量传输操作规则
  • 有序性:从主内存到工作内存复制变量,readload需顺序执行;从工作内存同步回主内存,storewrite要依次进行。比如线程操作共享变量balance(假设是账户余额 ),先read从主内存获取其值,再load到工作内存才能使用;修改后,先store准备传输,再write更新主内存的值。不过,JMM 只要求顺序执行,不强制连续,实际中两组操作间可能穿插其他操作。
  • 成对性readloadstorewrite必须成对出现。若只执行readload,工作内存拿不到变量值;只storewrite,主内存无法更新。这就像锁和钥匙配对,缺一不可,保证数据传输完整。
工作内存变量操作规则
  • 修改必同步:线程对工作内存变量执行assign修改后,必须同步到主内存。例如多线程操作计数器count,一个线程对其assign自增后,必须通过storewrite让主内存更新,否则其他线程读取的可能是旧值,引发数据不一致问题。
  • 无因不同步:没发生assign操作,线程不能将工作内存数据同步回主内存。这避免无意义的同步操作,确保每次同步都基于实际修改,提升内存操作效率。
  • 先初始化再操作:新变量只能在主内存创建,工作内存使用变量前,必须先loadassign初始化。比如定义int num;后,需先assign赋值或load初始值,才能进行use计算或store回传操作,防止使用未初始化变量导致错误。
lockunlock操作规则
  • 互斥与重入:同一时刻只有一条线程能对变量lock,实现互斥访问。同时,lock可被同线程重复执行(重入 ),但要执行相同次数unlock才解锁,lockunlock成对。像synchronized代码块,线程多次进入执行lock,离开执行unlock,保证代码块内操作原子性。
  • 操作关联与状态维护:对变量lock会清空工作内存值,后续使用需重loadassign。未lock变量不能unlock,也不能unlock其他线程锁定的变量。执行unlock前,要先storewrite将变量同步到主内存,保证主内存是最新状态,维护多线程数据一致性。



image




基本描述

这张图展示了 Java 内存模型(JMM)中的同步操作与规则。图中呈现了主内存、工作内存和 Java 线程之间的关系,以及涉及的八种同步操作:LockUnlockReadLoadUseAssignStoreWrite 。主内存通过Save/Load与多个工作内存交互,每个工作内存对应一个 Java 线程。各同步操作以箭头形式展示了数据在不同内存区域和线程间的流向。

深层次展开描述
各组件关系
  • 主内存与工作内存:主内存是所有线程共享的内存区域,存储共享变量;工作内存是每个线程私有的,用于存储从主内存读取的共享变量副本。线程对共享变量的操作在工作内存中进行,再通过同步操作与主内存交互,保证数据一致性。例如多线程操作一个共享的计数器变量,各线程在自己工作内存中操作副本,再同步回主内存。
  • 工作内存与 Java 线程:工作内存为对应 Java 线程提供操作空间,线程执行时从工作内存读取和写入数据。每个线程独立管理自己的工作内存,确保线程操作的隔离性和安全性。
同步操作详解
  • 锁定与解锁操作
    • Lock:作用于主内存变量,使变量处于被线程独占状态,确保同一时刻只有一个线程能操作该变量,是实现多线程互斥访问的基础。如多个线程同时访问共享资源时,先对资源相关变量Lock,防止其他线程干扰。
    • Unlock:与Lock对应,释放被锁定的变量,使其他线程可对其进行Lock操作。Unlock前需确保变量已同步回主内存,保证数据一致性。
  • 数据传输操作
    • ReadLoadRead从主内存读取变量值到工作内存,LoadRead获取的值放入工作内存变量副本。二者顺序执行,是线程获取主内存共享变量值的步骤。
    • StoreWriteStore将工作内存变量值传至主内存,WriteStore的值写入主内存变量。用于线程将工作内存中修改后的值同步回主内存。
  • 工作内存操作
    • Use:将工作内存变量值传递给执行引擎,供线程执行运算等操作使用。
    • Assign:把执行引擎接收到的值赋给工作内存变量,用于更新工作内存中变量的值。
同步规则体现

这些操作共同遵循 JMM 同步规则,如ReadLoadStoreWrite成对出现;变量修改后需同步回主内存;LockUnlock成对且遵循特定执行顺序等。这些规则保证多线程环境下共享变量访问的原子性、可见性和有序性,避免数据竞争和不一致问题,确保 Java 程序在多线程场景下正确执行。



在 Java 内存模型(JMM)的框架下,以下是基于图中同步操作与规则的业务操作流程:

准备阶段

当 Java 程序启动多个线程并涉及共享变量操作时,首先要明确共享变量存储在主内存中。例如一个多线程的银行转账业务,账户余额作为共享变量存于主内存。

线程读取变量阶段
  1. 锁定(Lock) :若线程要对共享变量进行操作,需先对主内存中的该变量执行Lock操作,将其标识为独占状态。就像转账时,先锁定账户余额变量,防止其他线程同时操作。
  2. 读取(Read)和载入(Load) :线程执行Read操作,从主内存把共享变量值传输到工作内存,紧接着Load操作将读取的值放入工作内存的变量副本。比如转账线程读取账户余额到自己的工作内存。
线程操作变量阶段
  1. 使用(Use)和赋值(Assign) :线程在工作内存中,通过Use操作把变量值传递给执行引擎进行运算等操作。运算完成后,Assign操作将执行引擎得到的值赋给工作内存的变量。例如转账时计算扣除金额后的新余额,先Use原余额进行减法运算,再Assign新余额值。
线程写回变量阶段
  1. 存储(Store)和写入(Write) :线程完成对工作内存中变量的操作后,Store操作把变量值传送到主内存,随后Write操作将该值写入主内存的变量中。比如转账完成后,将新余额写回主内存。
  2. 解锁(Unlock) :最后,线程对主内存中的变量执行Unlock操作,释放独占状态,允许其他线程对该变量进行操作。
操作规则保障

整个流程严格遵循 JMM 的同步规则,比如ReadLoadStoreWrite必须按顺序执行且成对出现;不允许线程无原因地同步数据;变量修改后必须同步回主内存等。这些规则确保在多线程并发操作共享变量时,数据的一致性、原子性和可见性,保障业务逻辑正确执行,避免出现转账金额错误、账户余额混乱等问题。



基本规则
  1. 数据交互规则:Java 内存模型规定了主内存和工作内存之间的数据交互规范。主内存是所有线程共享的内存区域,存储着共享变量的原始值;工作内存是每个线程私有的,用于缓存从主内存读取的共享变量副本。线程对共享变量的操作都在工作内存中进行,之后再同步回主内存 。
  2. 操作顺序规则:对变量从主内存复制到工作内存,需按顺序执行readload操作;从工作内存同步回主内存,要依次执行storewrite操作。但这些操作只需按顺序执行,不要求连续执行 。
  3. 操作配对规则readloadstorewrite操作必须成对出现,不允许其中一个操作单独执行,以此保证数据传输的完整性和一致性。
操作规则
  1. 变量使用与更新规则
    • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中被修改后,必须通过storewrite操作同步到主内存中,确保其他线程能读取到最新值。
    • 不允许一个线程无原因地(没有发生过任何assign操作 )把数据从工作内存同步回主内存,保证同步操作基于实际的变量修改。
    • 一个新的变量只能在主内存中诞生,在对一个变量实施usestore操作之前,必须先执行过loadassign操作,防止使用未初始化的变量。
  2. 锁定与解锁规则
    • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,只有执行相同次数的unlock操作后,变量才会被解锁,且lockunlock必须成对出现。
    • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行loadassign操作初始化变量的值。
    • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
    • 对一个变量执行unlock操作之前,必须先把此变量通过storewrite操作同步到主内存中。



4:并发优势与风险

image




并发的优势
  1. 速度:能同时处理多个请求,响应更快,可将复杂操作拆分为多个进程并行处理,加快任务完成速度,提升系统响应能力。比如电商平台在大促时,并发处理大量订单请求,快速反馈下单结果。
  2. 设计:让程序设计在某些场景下更简单,提供更多设计选择。开发人员可依据业务特点,将任务拆分为不同线程或进程,优化程序结构,提高代码可维护性与扩展性。像游戏开发中,用并发处理不同游戏元素更新逻辑。
  3. 资源利用:使 CPU 在等待 I/O 操作(如磁盘读写、网络请求 )时,能切换去执行其他任务,避免资源闲置,提高 CPU 利用率。例如服务器处理文件读取请求时,等待磁盘 I/O 过程中,可处理其他网络请求。
并发的风险
  1. 安全性:多线程共享数据时,可能因数据竞争等问题,产生与预期不符结果,如数据不一致、丢失更新等。例如多线程同时操作银行账户余额,未妥善处理会导致余额计算错误。
  2. 活跃性:出现死锁(多个线程相互等待对方释放资源,导致都无法继续执行 )、饥饿(某个线程长期无法获取资源执行 )等问题,使操作无法继续。比如多个线程竞争有限锁资源时,可能陷入死锁。
  3. 性能:线程过多会使 CPU 频繁切换上下文,增加调度时间开销;同步机制(如锁 )使用不当会增加资源等待时间;还可能占用过多内存资源,影响系统整体性能。像高并发场景下,未优化的线程池会引发这些性能问题。



并发的优势
速度方面
  • 并行处理原理:并发允许系统同时处理多个任务,这基于现代计算机多核 CPU 的特性。每个核心可独立执行线程,多个请求能被并行处理。以 Web 服务器为例,当大量用户同时请求页面时,并发机制让服务器为不同用户请求分配不同线程或进程,同时进行页面渲染和数据获取,大幅缩短整体响应时间。复杂操作拆分也是提升速度的关键,例如在图像渲染中,可将图像分割成多个区域,每个区域由一个线程负责渲染,最后合并结果,显著加快渲染速度。
  • 异步 I/O 协同:结合异步 I/O 技术,并发能进一步提升速度。在处理 I/O 操作(如网络请求、文件读写 )时,异步 I/O 允许程序在等待 I/O 完成期间不阻塞,继续执行其他任务。比如在网络爬虫程序中,多个线程可同时发起对不同网页的请求,在等待响应过程中,线程可处理已返回的网页数据,提高爬虫效率。
设计方面
  • 模块化与抽象:并发编程促使程序设计更倾向于模块化和抽象化。开发人员需将复杂任务分解为多个可并行执行的子任务,每个子任务可视为一个独立模块。这种设计方式降低了系统复杂度,提高代码可读性和可维护性。以微服务架构为例,每个微服务可看作一个独立的并发单元,通过接口与其他服务交互,使系统架构更清晰。
  • 灵活性与可扩展性:为程序设计提供更多选择和灵活性。在设计算法或数据结构时,可根据并发需求进行优化。例如使用并发数据结构(如 ConcurrentHashMap )替代传统数据结构,满足多线程安全访问需求。同时,良好的并发设计使系统更具扩展性,方便添加新功能或处理更大规模的任务。
资源利用方面
  • CPU 资源优化:在单线程程序中,CPU 常因等待 I/O 操作而闲置。并发机制让 CPU 在等待 I/O 时切换到其他可执行线程,充分利用 CPU 时间片。比如数据库查询操作中,在等待数据库返回结果时,CPU 可执行其他与查询无关的计算任务,提高 CPU 利用率。
  • 资源均衡分配:并发还能实现系统资源的均衡分配。通过合理调度线程,可避免资源过度集中在某些任务上。例如在分布式计算集群中,可根据各个节点的负载情况,动态分配任务,确保所有节点资源都得到充分利用,提升整个集群的性能。
并发的风险
安全性方面
  • 数据竞争根源:多线程共享数据时,若缺乏有效同步机制,就会出现数据竞争。不同线程可能同时读写共享变量,导致数据不一致。例如在多线程计数器中,若两个线程同时对计数器变量进行自增操作,可能会丢失一次自增,最终结果与预期不符。
  • 内存可见性问题:除数据竞争,内存可见性也是影响安全性的重要因素。由于 Java 中线程工作内存和主内存的数据同步存在延迟,一个线程对共享变量的修改可能无法及时被其他线程看到。例如一个线程修改了共享变量并通知其他线程基于新值执行操作,但其他线程可能仍读取到旧值,导致程序逻辑错误。
活跃性方面
  • 死锁形成机制:死锁是活跃性的典型问题,通常由多个线程对资源的循环等待造成。例如线程 A 持有资源 R1,等待资源 R2,而线程 B 持有资源 R2,等待资源 R1,此时两个线程相互等待,都无法继续执行。死锁一旦发生,相关线程和资源都会被阻塞,影响系统正常运行。
  • 饥饿现象剖析:饥饿是指某个线程因长期无法获取必要资源而无法执行。比如在资源分配不公平的情况下,低优先级线程可能一直无法获得 CPU 时间片或其他关键资源,尽管系统看似在正常运行,但该线程的任务却无法推进。
性能方面
  • 上下文切换开销:线程过多时,CPU 频繁进行上下文切换。每次上下文切换都需要保存和恢复线程的运行状态,包括寄存器值、程序计数器等信息,这会消耗大量 CPU 时间。例如在一个线程数量远超 CPU 核心数的系统中,频繁的上下文切换会使 CPU 大部分时间用于状态切换,而非执行实际任务,导致性能下降。
  • 同步机制代价:为保证线程安全,常使用同步机制(如锁 ),但这也会带来性能开销。当多个线程竞争同一把锁时,未获取到锁的线程需要等待,这会增加线程执行时间。而且锁的获取和释放操作本身也有一定开销,特别是在高并发场景下,大量线程竞争锁会严重影响系统性能。
  • 内存占用激增:创建大量线程会占用较多内存资源,因为每个线程都需要一定的栈空间来存储局部变量、方法调用等信息。当线程数量过多,可能导致系统内存不足,引发性能问题甚至程序崩溃。此外,线程间共享数据结构也可能因并发访问而占用更多内存用于维护同步状态。


image



CPU 多级缓存
  • 缓存一致性:现代 CPU 为提升性能设置多级缓存(如 L1、L2、L3 ) 。当多个 CPU 核心同时访问和修改主存中同一数据时,各缓存副本需保持一致。常用 MESI 协议等确保,若一个核心修改缓存数据,会通过总线通知其他核心使对应缓存行无效或更新 ,防止数据不一致。
  • 乱序执行优化:CPU 为充分利用执行单元,在不改变程序最终结果前提下,允许指令不按程序顺序执行。通过寄存器重命名等技术,记录指令间依赖关系,让无依赖指令并行执行,提升执行效率,但需缓存一致性机制保障数据正确。
Java 内存模型(JMM)
  • JMM 规定:JMM 是 Java 虚拟机规范中定义的抽象概念,用于屏蔽各操作系统和硬件内存访问差异,保证 Java 程序在不同平台内存访问的一致性。它规定了多线程环境下,线程如何与主内存、工作内存交互,确保可见性、原子性和有序性。
  • 抽象结构:包含主内存和每个线程的工作内存。主内存是所有线程共享的内存区域,存储对象实例等数据;工作内存是线程私有的,保存主内存中变量副本。线程对变量的操作在工作内存中进行,操作后再同步回主内存 。
  • 同步八种操作及规则:包括 lock(锁定)、unlock(解锁) 、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入) 。这些操作有严格执行顺序和规则,如一个变量在工作内存使用前需先从主内存 load,修改后需 store 和 write 回主内存等,保障内存操作正确有序。
Java 并发的优势与风险
  • 优势:可充分利用多核 CPU 资源,提升程序执行效率,加快任务处理速度,如 Web 服务器多线程处理大量客户端请求;能改善用户体验,使程序在执行耗时任务时仍可响应其他操作,如 GUI 程序后台任务与界面交互可并发进行。
  • 风险:可能出现线程安全问题,如多个线程同时读写共享数据,导致数据竞争、脏读、幻读等,使结果不可预期;死锁风险,多个线程相互等待对方释放资源,造成程序停滞;还可能因上下文切换频繁,消耗系统资源,降低性能 。









三、并发基础

1: 并发编程与线程安全

image


image



package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.annoations;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 用于标记【线程安全】的类或者写法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ThreadSafe {

String Value() default "";



}



package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.annoations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 用于标记【推荐】的类或者写法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Recommend {

String Value() default "";
}



package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.annoations;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 用于标记【线程不安全】的类或者写法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotThreeSafe {

String Value() default "";
}



package com.alan.springBootStudying.HighConcurrencyAndMultiThreaded.HighConcurrency.annoations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 用于标记【推荐】的类或者写法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotRecommend {

String Value() default "";
}




并发模拟—PostMan

image

image























四、项目准备






五、线程安全性








六、安全发布对象








七、线程安全策略








八、J.U.C之AQS








九、J.U.C组件拓展









十、线程调度-线程池






十一、多线程并发拓展







十二、高并发之扩容思路








十三、高并发之缓存思路






十四、高并发之消息队列思路





十五、高并发之应用拆分思路






十六、高并发之应用限流思路







十七、高并发之服务降级与服务熔断思路






十八、高并发之数据库切库分表思路





十九、高并发之高可用手段介绍







二十、课程总结








posted @ 2025-04-24 00:11  一品堂.技术学习笔记  阅读(169)  评论(0)    收藏  举报