并发编程三

Java并发编程核心笔记

一、synchronized锁的核心原理

1. 锁的基本规则

当一个线程试图访问同步代码块时,必须先获取锁 ,退出代码块或抛出异常时必须释放锁

  • 同步:多个线程对同一个资源轮流操作,需要互相等待
  • 异步:多个线程可以同时执行,无需互相等待

2. 底层实现:monitor指令

synchronized的底层是通过JVM的monitorentermonitorexit指令实现的:

  • monitorenter:编译后插入到同步代码块的开始位置
  • monitorexit:插入到方法结束处和异常处(保证任何情况下锁都会释放)
  • 每个monitorenter必须有对应的monitorexit配对

核心逻辑 :任何Java对象都有一个monitor(监视器)与之关联,当monitor被某个线程持有后,就处于锁定状态。线程执行到monitorenter时,会尝试获取对象对应monitor的所有权,也就是获取对象的锁。

3. 锁的两个关键标记

synchronized的锁本质上是给两个地方加了标记:

  1. 资源本身加标记 :锁信息存在Java对象头
  2. 操作资源的指令范围加标记:同步块内的指令是互斥的,同一时刻只能有一个线程执行这部分代码

4. Java对象头的存储结构

对象头是synchronized实现锁的基础,不同类型的对象头长度不同:

  • 非数组对象:2字宽(32位系统=64bit=8字节;64位系统=128bit=16字节)
  • 数组对象:3字宽(多了4字节存储数组长度)

32位系统下,对象头中Mark Word(标记字)的核心结构如下:

锁状态 25bit 4bit 1bit是否偏向锁 2bit锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01
偏向锁 线程ID+epoch 对象分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 - - 00
重量级锁 指向monitor的指针 - - 10
GC标记 - - - 11

5. 补充:hashCode与对象地址的关系

  • 两个对象地址不同 ,hashCode可能相同(hash冲突)
  • 两个对象hashCode不同 ,地址肯定不同(默认hashCode基于内存地址生成,重写后不适用)

二、锁的升级机制(synchronized的三种状态)

synchronized的锁会随着竞争程度从低到高升级,升级过程不可逆(只能从偏向锁→轻量级锁→重量级锁)。

1. 为什么需要锁升级?

早期synchronized是重量级锁,线程竞争失败会直接进入阻塞队列,上下文切换开销大。JDK1.6引入锁升级后,根据竞争程度选择不同的锁实现,大幅提升了性能。

2. 三种锁的适用场景与原理

锁类型 适用场景 核心原理
偏向锁 无竞争(只有一个线程) 把线程ID直接记录在对象头中,下次该线程访问时无需CAS,直接获取锁
轻量级锁 轻度竞争(少量线程) 竞争失败的线程不进入阻塞队列 ,而是通过自旋不断尝试获取锁
重量级锁 重度竞争(大量线程) 竞争失败的线程进入阻塞队列,等待持有锁的线程唤醒,上下文切换开销大

3. 关键问题解答

问:线程没竞争到资源为什么要进阻塞队列?

答:如果让竞争失败的线程留在就绪队列,当持有锁的线程A时间片用完后,CPU可能又会调度到这个线程,但此时资源还被A锁着,会白白浪费CPU时间片。

三、原子操作与CAS

1. 什么是原子操作?

原子操作是不可拆分的操作,要么全部执行成功,要么全部执行失败并回滚。

  • 例子:int a=1是原子操作;a++不是原子操作(分为读a、加1、写回a三步)

2. 如何保证原子性?

CPU层面提供了两种机制保证原子操作:

  1. 总线锁 :处理器输出LOCK#信号,阻塞其他处理器的内存请求,独占共享内存(开销大)
  2. 缓存锁:利用缓存一致性协议,阻止多个处理器同时修改同一块缓存行的数据(常用)

Java层面:

  • 使用synchronized关键字
  • 使用java.util.concurrent.atomic包下的原子类(基于CAS实现)

3. CAS(比较并交换)详解

CAS是CPU提供的原子指令,是无锁编程的基础。

  • 三个操作数:内存位置V、预期值A、新值B
  • 执行逻辑:只有当V的值等于A时,才将V更新为B;否则什么都不做,返回当前V的值

代码示例:用CAS实现线程安全计数器

java 复制代码
private AtomicInteger atomicI = new AtomicInteger(0);

private void safeCount() {
    for (;;) { // 自旋
        int i = atomicI.get();
        // 比较当前值是否等于i,如果是则更新为i+1
        boolean suc = atomicI.compareAndSet(i, ++i);
        if (suc) {
            break;
        }
    }
}

4. CAS的优缺点与三大问题

优点 :无锁、并发弱时性能远高于synchronized
缺点:并发激烈时,大量线程自旋会消耗大量CPU

三大核心问题及解决方法

  1. ABA问题 :变量的值从A变成B,又变回A,CAS会误以为没有被修改过
    • 解决:使用版本号或时间戳,如AtomicStampedReference(同时比较值和版本号)
  2. 循环时间长开销大 :并发激烈时,线程自旋很久都获取不到锁
    • 解决:自适应自旋(JDK1.6默认),或并发激烈时改用synchronized
  3. 只能保证一个共享变量的原子操作
    • 解决:把多个共享变量封装成一个对象,使用AtomicReference;或直接用synchronized

四、内存可见性与指令重排序

1. 内存可见性问题

定义 :一个线程修改了共享变量,其他线程不能及时看到这个修改。

根源:Java内存模型(JMM)规定:

  • 所有变量都存储在主内存
  • 每个线程有自己的工作内存(抽象概念,对应CPU的高速缓存、寄存器)
  • 线程对变量的操作必须在工作内存中进行,不能直接读写主内存

当线程A修改了变量后,只是更新了自己的工作内存,还没刷新到主内存;此时线程B读取的还是自己工作内存中的旧值,就会出现可见性问题。

2. 如何保证内存可见性?

Java中通过以下三种方式保证内存可见性:

  1. volatile关键字
    • 写操作:修改后立即刷新到主内存
    • 读操作:直接从主内存读取,跳过工作内存
    • 额外作用:禁止指令重排序
  2. synchronized关键字
    • 加锁时:清空工作内存,从主内存读取最新值
    • 解锁时:将工作内存的修改刷新到主内存
  3. final关键字
    • final修饰的变量一旦初始化完成,其他线程就能看到它的正确值
    • JMM禁止final变量的写操作与构造方法重排序,避免出现未初始化的情况

3. 指令重排序

定义:编译器和处理器为了提高执行效率,会对指令进行重新排序,可能导致指令的执行顺序与代码的编写顺序不一致。

重排序的类型

  1. 编译器重排序:编译器在不改变单线程语义的前提下,重新安排指令的执行顺序
  2. 处理器重排序:
    • 指令级并行重排序:处理器将多条指令重叠执行
    • 内存系统重排序:处理器的缓存和写缓冲区导致指令执行顺序看起来乱序

经典问题示例

java 复制代码
// 初始状态:a = b = 0
// 处理器A执行:
a = 1; // A1
x = b; // A2

// 处理器B执行:
b = 2; // B1
y = a; // B2

由于指令重排序,可能出现A2→A1B2→B1的执行顺序,最终结果为x = y = 0,这就是重排序导致的并发问题。

五、线程与进程通信

1. 基本概念

通信的本质是交换信息。线程是进程内的执行单元,共享进程的地址空间;进程有独立的地址空间,通信需要操作系统介入。

2. 线程间通信的方式

  1. 共享内存 :多个线程对同一个对象或静态变量进行操作(最常用)
    • 注意:必须配合同步机制(synchronized、volatile、原子类)保证线程安全
  2. 消息传递
    • 基础方式:wait()/notify()/notifyAll()(基于对象的等待集)
    • 工具类:CountDownLatchCyclicBarrierSemaphoreBlockingQueue
  3. 管道流PipedInputStreamPipedOutputStream,用于线程间的字节流传输

3. 进程间通信(IPC)的方式

  1. 管道
    • 匿名管道:只能用于父子进程之间的通信
    • 命名管道:可用于任意进程之间的通信
  2. 消息队列:消息的链表,独立于进程,进程可以通过读写消息队列交换数据
  3. 共享内存:最快的IPC方式,多个进程映射同一块物理内存,直接读写
  4. 信号量:用于进程间的同步和互斥
  5. 信号:用于通知进程发生了某个事件(如Ctrl+C发送SIGINT信号)
  6. 套接字(Socket):跨网络的进程通信,支持不同主机上的进程通信

4. 纠正原文错误

原文"进程有的通信方式线程全都有;线程有的进程就不一定了"表述不准确,正确的是:

  • 线程可以使用所有进程间的通信方式(如管道、Socket等)
  • 线程有更轻量的通信方式(直接共享变量),而进程不能直接共享变量(地址空间独立)

六、补充知识点

  1. 静态对象的内存分布
    • JDK8及以前:静态变量的句柄存储在方法区的静态常量池,实例对象存储在堆内存
    • JDK8及以后:方法区被元空间(Metaspace)取代,静态变量和常量池移到了堆内存中
  2. JMM中的本地内存:是一个抽象概念,对应CPU的寄存器、高速缓存、写缓冲区等硬件
  3. 锁的选择建议
    • 并发弱:优先使用CAS(原子类)
    • 并发中等:使用轻量级锁(synchronized会自动升级)
    • 并发激烈:使用重量级锁或线程池控制并发数
相关推荐
idingzhi1 小时前
A股量化策略日报(2026年05月22日)
android·开发语言·python·kotlin
江上清风山间明月2 小时前
如何将python开发的window应用打包成exe
开发语言·python·exe·打包
SXJR2 小时前
Java中的Cross-Encoder模型解决方案
java·开发语言
彦为君2 小时前
JavaSE-11-BIO/NIO/AIO(多人聊天室)
java·开发语言·python·ai·nio
为何创造硅基生物2 小时前
C 语言 typedef 结构体私有化
c语言·开发语言·算法
计算机安禾2 小时前
【c++面向对象编程】第43篇:可变参数模板(C++11):优雅处理不定长参数
java·开发语言·c++
Hanniel2 小时前
Python __slots__ 入门指南
开发语言·python·性能优化
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第69题】【JVM篇】第29题:GC Roots 有哪些?
java·开发语言·jvm·面试
Matlab程序猿小助手2 小时前
【MATLAB源码-第319期】基于matlab的帝王蝶优化算法(MBO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab