深入浅出Java多线程(六):Java内存模型
大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第六篇内容:Java内存模型。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!
在并发编程中,有两个关键问题至关重要,它们是线程间通信机制和线程间同步控制。 线程间通信是指在一个多线程程序中,不同线程之间如何有效地交换信息。在Java内存模型(JMM)采用的共享内存并发模型中,线程间的通信主要是通过共享变量来实现的。每个线程可以读取或修改这些存储在堆内存中的共享变量,从而传递状态或数据给其他线程。例如: 在这个案例中, 线程间同步则是指控制不同线程间操作发生的相对顺序,以避免数据不一致和竞态条件等问题。在Java中,同步控制主要通过以下方式实现: 在这个例子中, 综上所述,在Java并发编程中,线程间通信与同步控制相辅相成,共同构建了一个安全高效的并发环境。通过合理地利用Java内存模型提供的机制,开发者可以确保在多线程环境中,各个线程之间的数据交换有序且可靠。 在并发编程领域,有两种主要的并发模型:消息传递并发模型和共享内存并发模型。Java多线程编程采用了共享内存并发模型,这一选择对理解Java内存模型(JMM)至关重要。 消息传递模型中,线程之间的通信和同步是通过发送和接收消息来实现的。每个线程拥有独立的本地状态,并通过将数据封装在消息中发送给其他线程来交换信息。在这种模型下,线程之间不直接共享数据,因此不存在竞争条件或同步问题。Erlang等语言中的Actor模型就是一个典型的消息传递并发模型实例。 在此Erlang示例中, 而在Java中采用的共享内存模型,则允许线程访问相同的内存区域——堆区,其中包含的共享变量可以被多个线程同时读写。这种模型下,线程间通信是通过对共享变量进行读写操作间接完成的。然而,由于共享数据,这就带来了潜在的数据一致性问题,如竞态条件、死锁以及可见性问题。为了保证线程间的正确交互,Java内存模型定义了一套规则和机制。 在这个Java示例中,两个线程同时访问 Java选择共享内存并发模型的原因在于其简洁性和高效性,尤其是对于基于对象和引用透明性的程序设计而言。尽管存在潜在的并发问题,但通过提供诸如 在Java虚拟机(JVM)的运行时环境中,内存被划分为多个区域以支持程序的执行。其中,线程私有的内存区域包括程序计数器、虚拟机栈以及本地方法栈,而堆和方法区则是所有线程共享的内存区域。 现代计算机系统为了提高性能,普遍采用了高速缓存技术,CPU有自己的缓存层级,包括L1、L2、L3等高速缓存。当线程A修改了堆内存中的共享变量时,这个更新可能只反映在了线程A所在的CPU缓存中,而不是立即同步到主内存或其他线程所在的CPU缓存中。这就是为什么即使是在共享内存区域——堆内存在多线程环境下也可能出现内存不可见性的问题。 上述代码中,如果没有使用 Java内存模型(JMM)是一种抽象概念,它定义了Java程序中各种变量的访问规则,尤其是针对堆内存中的共享变量。JMM确保了并发环境下的原子性、有序性和可见性: 综上所述,Java内存模型在多线程编程中扮演着核心角色,通过规范和约束线程如何访问和更新共享变量,有效地解决了并发环境下的内存一致性问题。 Java内存模型(JMM)和Java运行时内存区域是两个不同的概念层次,它们在描述并发编程的内存行为时有着各自的侧重点: 尽管JMM与Java运行时内存区域在概念上有所差异,但它们之间存在着密切的联系和映射关系: 虽然无法直接以代码形式展示这种抽象的映射关系,但在实际编程中,我们可以观察到以下现象: 在这个示例中, 在Java并发编程中,volatile关键字是一个重要的工具,它用于修饰共享变量,确保了该变量在多线程环境下的可见性和禁止指令重排序。当一个线程修改了volatile变量的值时,其他线程能够立即看到这个更新后的值,这是因为volatile变量的读写操作都会与主内存直接交互,并且会在必要时插入内存屏障以保证数据的一致性。 在这个例子中, 在此示例中, 为了更深入地理解并发控制机制,Java内存模型还引入了内存屏障(Memory Barrier)的概念,这是一种硬件级别的指令,用于确保特定内存操作顺序并刷新缓存。Java编译器会根据JMM规则,在适当的时机插入内存屏障,以实现对volatile变量和其他同步原语的正确支持。 另外,Java内存模型通过happens-before原则来简化程序员理解和推理程序行为。它定义了一系列先行发生关系,比如:程序次序规则、监视器锁规则等,这些规则明确了事件之间的执行顺序,如果A happens-before B,那么线程A对共享变量的修改对于线程B来说一定可见。 例如: 在这个例子中,因为 本文使用
引言
线程间通信机制
class SharedData {
public volatile int sharedValue;
}
public class ThreadCommunication {
public static void main(String[] args) {
SharedData data = new SharedData();
Thread threadA = new Thread(() -> {
data.sharedValue = 10; // 线程A更新共享变量
});
Thread threadB = new Thread(() -> {
while (data.sharedValue == 0) {} // 线程B等待共享变量被更新
System.out.println("Thread B sees updated value: " + data.sharedValue);
});
threadA.start();
threadB.start();
try {
threadA.join(); // 确保线程A完成更新
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
sharedValue
是一个共享变量,线程A对其进行了修改,而线程B则依赖于该变量的值进行后续操作。为了确保线程间通信的正确性,这里使用了
volatile
关键字来保证变量的可见性和禁止指令重排序。
线程间同步控制
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
increment()
和
getCount()
方法都被
synchronized
修饰,这样在同一时刻只能有一个线程执行这两个方法之一,防止了并发环境下计数器值的错误累加。
synchronized
机制,Java还提供了更灵活的Lock接口如ReentrantLock,它允许更多的同步语义,比如尝试获取锁、可中断获取锁以及公平锁等。
并发模型对比
消息传递并发模型
-module(my_actor).
-export([start_link/0, ping/0]).
start_link() ->
register(actor_name, spawn(fun() -> loop([]) end)).
ping() ->
actor_name ! {self(), ping}.
loop(Msgs) ->
receive
{From, ping} ->
From ! pong,
loop(Msgs);
_Other ->
loop([Msg | Msgs])
end.
actor_name
是一个进程(即线程),它通过接收并响应消息来进行工作,而不是直接读写共享变量。
共享内存并发模型
public class SharedCounter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedCounter counter = new SharedCounter();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
});
Thread threadB = new Thread(() -> {
while (true) {
System.out.println(counter.getCount());
}
});
threadA.start();
threadB.start();
threadA.join();
}
}
SharedCounter
类的共享变量
count
,为了确保线程安全和可见性,我们使用了
volatile
关键字修饰该变量。Java内存模型通过主内存与各线程私有本地内存间的抽象关系以及内存屏障技术,保障了线程间共享变量的更新能够及时传播到所有线程。
synchronized
、
volatile
以及更高层次的并发工具如
java.util.concurrent
包下的各种锁机制和原子类等,Java提供了丰富的工具来管理和控制共享内存环境下的并发行为,使得开发者能够编写出高效的并发代码。
Java内存模型抽象结构解析
运行时数据区划分
public class StackExample {
public void localVariableVisibility() {
int localVar = 10; // 局部变量存储在线程栈中,对其他线程不可见
}
}
堆内存中的内存不可见性原因
public class CacheCoherenceIssue {
private static volatile int sharedValue = 0;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
sharedValue++;
}
});
Thread threadB = new Thread(() -> {
while (sharedValue == 0); // 如果没有volatile,可能会陷入循环无法退出
System.out.println("Thread B observed: " + sharedValue);
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
volatile
关键字修饰
sharedValue
,线程B可能无法观察到线程A对共享变量的更新,因为这种更新可能未及时传播至主内存或线程B的工作内存。
Java内存模型(JMM)详解
Java内存模型与Java内存区域的关系
两者区别
联系与映射
public class MemoryModelMapping {
private static int sharedValue; // 存储在堆中,属于主内存区域
private int threadLocalValue; // 存储在线程栈中,属于本地内存
public void runInParallel() {
Thread threadA = new Thread(() -> {
sharedValue = 10; // 修改共享变量
threadLocalValue = 20; // 修改线程局部变量
});
Thread threadB = new Thread(() -> {
while (sharedValue == 0) {} // 等待共享变量更新
System.out.println("Shared value: " + sharedValue);
});
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
sharedValue
变量由于被多个线程共享,它的修改需要遵循JMM的同步和可见性规则,而
threadLocalValue
变量仅在线程内部使用,不受JMM的跨线程可见性约束,其生命周期完全受限于所在线程的虚拟机栈范围。这样,我们便能直观地感受到JMM与Java运行时内存区域之间的关联和作用机制。
Java语言特性与JMM实现
volatile关键字的作用
public class VolatileExample {
private volatile int sharedValue = 0;
public void increment() {
sharedValue++;
}
public int getSharedValue() {
return sharedValue;
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread threadA = new Thread(example::increment);
Thread threadB = new Thread(() -> System.out.println("Thread B sees: " + example.getSharedValue()));
threadA.start();
threadA.join(); // 确保线程A完成操作
threadB.start();
}
}
sharedValue
是一个被volatile修饰的变量,线程A对其进行了递增操作,而线程B可以立即获取到最新的值,体现了volatile对于共享状态同步的重要作用。
synchronized关键字的功能
synchronized
关键字提供了原子性和可见性保障,它可以应用于方法或代码块,使得在同一时间只有一个线程能访问被保护的资源,从而有效地解决了竞态条件和数据一致性问题。public class SynchronizedExample {
private int counter = 0;
public synchronized void incrementCounter() {
counter++;
}
public synchronized int getCount() {
return counter;
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.incrementCounter();
}
});
Thread threadB = new Thread(() -> {
while (true) {
if (example.getCount() >= 1000) {
System.out.println("Counter reached 1000");
break;
}
}
});
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized
方法
incrementCounter()
和
getCount()
保证了计数器的增量操作是原子性的,同时多个线程对counter的读写操作不会出现竞态条件,即线程B总能看到线程A对counter修改的最新结果。
内存屏障与happens-before原则
public class HappensBeforeExample {
private static boolean flag = false;
private static int data = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(() -> {
data = 1; // 修改数据
flag = true; // 设置标志位
});
one.start();
one.join();
// 根据happens-before原则,由于监视器锁规则
// 当进入同步块时,线程将看到之前对flag的修改
synchronized (HappensBeforeExample.class) {
if (flag) {
System.out.println("Data seen in other thread: " + data); // 输出正确的值
}
}
}
}
synchronized
关键字遵循happens-before原则中的监视器锁规则,因此主线程在进入同步块时,可以看到之前线程one对
flag
的修改,进而确定
data
变量是否已经被正确设置。
markdown.com.cn
排版