多线程三大特性——可见性

本文详细介绍了Java多线程中的可见性问题,解释了JMM(Java内存模型)如何确保线程间共享变量的正确通信。讨论了happens-before原则、volatile关键字以及synchronized的作用,它们分别提供了对可见性的保障,确保线程能够看到其他线程对共享变量的更新。通过对这些概念的深入理解,开发者可以更好地处理多线程环境中的并发问题。

JMM

JMM对共享内存的操作做出了如下两条规定:

  • 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
  • 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。

可见性问题

在多线程应用程序中,线程对非易失性变量(non-volatile variables进行操作,出于性能原因,每个线程在处理变量时会将变量从主内存复制到CPU缓存中。如果您的计算机包含多个CPU,每个线程可能运行在不同的CPU上。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。

Threads may hold copies of variables from main memory in CPU caches.

假设两个或多个线程访问一个共享对象,该对象包含如下声明的计数器变量:

public class SharedObject {

    public int counter = 0;

}

假设只有线程1增加了计数器变量,但线程1和线程2可能会不时地读取计数器变量。如果计数器变量不是声明为volatile,则无法保证计数器变量的值何时从CPU缓存写到主存。这意味着,CPU缓存中的计数器变量值可能与主存中的计数器变量值不同。

The CPU cache used by Thread 1 and main memory contains different values for the counter variable.

线程看不到变量的最新值,因为它还没有被另一个线程写回主内存,这个问题被称为“可见性”问题。一个线程的更新对其他线程是不可见的

可见性解决方案

Happens-Before

从JDK5开始,Java使用新的JSR -133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:

一个线程中的每个操作,happens-before于该线程中的任意后续操作。

  • 监视器锁规则:

对一个监视器锁的解锁,happens-before于随后对这个监视器锁的加锁。

  • volatile变量规则:

对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

  • 传递性:

如果A happens-before B,且B happens-before C,那么A happens-before C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

happens-before与JMM的关系如下图所示:

这里写图片描述

如上图所示,一个happens-before规则通常对应于多个编译器和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,它避免java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。

volatile

Java volatile关键字旨在解决变量可见性问题。通过声明计数器变量volatile,所有对计数器变量的写入都将立即写回主存。同样,对计数器变量的所有读取都将直接从主存中读取。

public class SharedObject {

    public volatile int counter = 0;

}

在上面给出的场景中,一个线程(T1)修改计数器,而另一个线程(T2)读取计数器(但从不修改它),声明计数器变量volatile足以保证对T2写入计数器变量的可见性。因此,声明变量volatile可以保证其他线程对该变量的写入是可见的。

使用volatile修饰一个共享变量可以达到如下的效果:

  • 一旦线程对这个共享变量的副本做了修改,会立马刷新最新值到主内存中去;
  • 一旦线程对这个共享变量的副本做了修改,其他线程中对这个共享变量拷贝的副本值会失效,其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。

volatile可见性规范:

  • 对volatile变量执行写操作时,会在写操作后加入一条store写屏障指令,强制将缓存刷新到主内存中
  • 对volatile变量执行读操作时,会在读操作前加入一条load读屏障指令,强制使缓冲区缓存失效,所以会从主内存读取最新值。
  • 防止指令重排序。
共享变量在线程间不可见的原因volatile解决方案
重排序 & 线程交叉执行防止指令重排序
共享变量未及时更新通过volatile可见性规范

synchronized

使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。

JMM关于synchronized的规定:

  •  线程解锁前,必须把共享变量的最新值刷新到主内存中
  •  线程加锁时,将清空工作内存中存储的共享变量的值,从而使用共享变量时,必须从主内存中重新读取最新的值。(注意:解锁和加锁,是指同一把锁)

因此线程执行synchronized代码执行互斥锁的过程:

  1. 获得互斥锁。
  2. 清空工作内存。
  3. 从主内存拷贝变量的最新副本到工作内存。
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存中
  6. 释放互斥锁

synchronized还会限制编译器、运行时和硬件对内存操作重排序的方式,从而在实施重排序时不会破坏JMM提供的可见性保证。

共享变量在线程间不可见的原因synchronized解决方案
重排序 & 线程交叉执行原子性(结合as-if-serial语义)
共享变量未及时更新通过synchronized可见性规范

文献

Java Volatile Keyword

就是要你懂Java中volatile关键字实现原理

Java volatile关键字的实现原理深度解析

### 多线程编程中的三大特性详解 #### 1. 原子性 (Atomicity) 原子性是指一个或多个操作在执行过程中不会被其他线程打断,这些操作要么全部完成,要么完全不发生。在线程切换时,可能会破坏某些看似简单的操作的原子性。例如 `count += 1` 并不是一个单一的指令,而是由读取、修改和写入三个步骤组成[^4]。如果在这几个步骤之间发生了线程切换,则可能导致数据不一致。 为了保证操作的原子性,Java 提供了一些工具类,比如 `java.util.concurrent.atomic` 包下的 `AtomicInteger` 和 `AtomicLong` 等。它们通过硬件支持的 CAS(Compare And Swap)算法实现了高效的无锁机制[^5]。 ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); // 原子性操作 } } ``` --- #### 2. 可见性 (Visibility) 可见性指的是当一个线程修改了共享变量的值后,其他线程能够立即看到这个变化。如果没有特殊的处理,默认情况下,由于缓存的存在以及编译器优化等原因,可能无法及时感知到最新的状态[^3]。 解决方法之一是使用 `volatile` 关键字标记那些需要跨线程访问并保持最新视图的字段。它强制每次读取都从主内存获取数据,并且禁止重排序以确保一致性。 然而需要注意的是,虽然 volatile 能够提供一定程度上的保障,但它并不能替代锁来保护复合动作的安全性。 ```java class VolatileExample { private volatile boolean flag = true; public void stop() { this.flag = false; } // 修改标志位 public void runTask() { while(flag){ System.out.println("Running..."); } } } ``` --- #### 3. 有序性 (Ordering) 程序运行期间实际发生的事件顺序并不总是按照源码书写次序严格执行;这是因为现代处理器会采用乱序执行技术提高效率,而 JVM 编译期也可能调整语句位置以便更好地利用资源。这种行为通常被称为 **重排序** 或者说是违反了逻辑上的因果关系。 可以通过 synchronized 锁定代码块的方式阻止此类现象的发生,因为它不仅提供了互斥功能还隐含着建立了一个全序关系——即进入临界区前后的任何变动都会对外部世界显现出来。另外还可以借助于特定屏障如 Memory Barrier 来达成目的。 ```java public class SynchronizationExample { private int value; public synchronized void setValue(int newValue) { value = newValue; // 设置新值 } public synchronized int getValue(){ return value; // 获取当前值 } } ``` --- ### 总结 综上所述,在多线程环境中维护良好的应用表现离不开妥善管理上述三种属性: - 使用原生 API 如 atomic 类型或者显式加锁手段维持操作不可分割; - 利用 volatile 注解增强关键区域的数据传播速度从而满足实时需求; - 防范因架构差异引发潜在隐患则依赖同步控制结构构建稳定序列框架。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值