源码分享站,wordpress会员主页,wordpress怎么创建菜单,河源建设工程交易中心网站引言 大家好#xff0c;我是你们的老伙计秀才#xff01;今天带来的是[深入浅出Java多线程]系列的第六篇内容#xff1a;Java内存模型。大家觉得有用请点赞#xff0c;喜欢请关注#xff01;秀才在此谢过大家了#xff01;#xff01;#xff01; 在并发编程中#xf… 引言 大家好我是你们的老伙计秀才今天带来的是[深入浅出Java多线程]系列的第六篇内容Java内存模型。大家觉得有用请点赞喜欢请关注秀才在此谢过大家了 在并发编程中有两个关键问题至关重要它们是线程间通信机制和线程间同步控制。 线程间通信机制 线程间通信是指在一个多线程程序中不同线程之间如何有效地交换信息。在Java内存模型JMM采用的共享内存并发模型中线程间的通信主要是通过共享变量来实现的。每个线程可以读取或修改这些存储在堆内存中的共享变量从而传递状态或数据给其他线程。例如 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 关键字来保证变量的可见性和禁止指令重排序。 线程间同步控制 线程间同步则是指控制不同线程间操作发生的相对顺序以避免数据不一致和竞态条件等问题。在Java中同步控制主要通过以下方式实现 synchronized关键字它可以修饰方法或代码块确保同一时间只有一个线程能访问被保护的资源。如下所示 class Counter {private int count 0;public synchronized void increment() {count;}public synchronized int getCount() {return count;}
} 在这个例子中increment() 和 getCount() 方法都被 synchronized 修饰这样在同一时刻只能有一个线程执行这两个方法之一防止了并发环境下计数器值的错误累加。 Lock接口及其实现类除了内置的synchronized机制Java还提供了更灵活的Lock接口如ReentrantLock它允许更多的同步语义比如尝试获取锁、可中断获取锁以及公平锁等。 volatile关键字虽然主要用于提供内存可见性但其也能间接起到一定的同步作用即当一个线程修改了volatile变量时其他线程能够立即看到这个新值。 综上所述在Java并发编程中线程间通信与同步控制相辅相成共同构建了一个安全高效的并发环境。通过合理地利用Java内存模型提供的机制开发者可以确保在多线程环境中各个线程之间的数据交换有序且可靠。 并发模型对比 在并发编程领域有两种主要的并发模型消息传递并发模型和共享内存并发模型。Java多线程编程采用了共享内存并发模型这一选择对理解Java内存模型JMM至关重要。 Untitled.png 消息传递并发模型 消息传递模型中线程之间的通信和同步是通过发送和接收消息来实现的。每个线程拥有独立的本地状态并通过将数据封装在消息中发送给其他线程来交换信息。在这种模型下线程之间不直接共享数据因此不存在竞争条件或同步问题。Erlang等语言中的Actor模型就是一个典型的消息传递并发模型实例。 -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. 在此Erlang示例中actor_name 是一个进程即线程它通过接收并响应消息来进行工作而不是直接读写共享变量。 共享内存并发模型 而在Java中采用的共享内存模型则允许线程访问相同的内存区域——堆区其中包含的共享变量可以被多个线程同时读写。这种模型下线程间通信是通过对共享变量进行读写操作间接完成的。然而由于共享数据这就带来了潜在的数据一致性问题如竞态条件、死锁以及可见性问题。为了保证线程间的正确交互Java内存模型定义了一套规则和机制。 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();}
} 在这个Java示例中两个线程同时访问SharedCounter类的共享变量count为了确保线程安全和可见性我们使用了volatile关键字修饰该变量。Java内存模型通过主内存与各线程私有本地内存间的抽象关系以及内存屏障技术保障了线程间共享变量的更新能够及时传播到所有线程。 Java选择共享内存并发模型的原因在于其简洁性和高效性尤其是对于基于对象和引用透明性的程序设计而言。尽管存在潜在的并发问题但通过提供诸如synchronized、volatile以及更高层次的并发工具如java.util.concurrent包下的各种锁机制和原子类等Java提供了丰富的工具来管理和控制共享内存环境下的并发行为使得开发者能够编写出高效的并发代码。 Java内存模型抽象结构解析 运行时数据区划分 Untitled 1.png 在Java虚拟机JVM的运行时环境中内存被划分为多个区域以支持程序的执行。其中线程私有的内存区域包括程序计数器、虚拟机栈以及本地方法栈而堆和方法区则是所有线程共享的内存区域。 栈每个线程都有自己的栈空间用于存储局部变量、方法调用时的上下文信息如返回地址、临时变量等。由于栈是线程私有的因此线程间不涉及共享和可见性问题。 public class StackExample {public void localVariableVisibility() {int localVar 10; // 局部变量存储在线程栈中对其他线程不可见}
} 堆堆内存是所有线程共享的区域主要存储对象实例及数组。当创建一个类的对象时其对象实例就分配在堆内存上这些实例变量对所有能够访问到该对象的线程都是可见的。 堆内存中的内存不可见性原因 现代计算机系统为了提高性能普遍采用了高速缓存技术CPU有自己的缓存层级包括L1、L2、L3等高速缓存。当线程A修改了堆内存中的共享变量时这个更新可能只反映在了线程A所在的CPU缓存中而不是立即同步到主内存或其他线程所在的CPU缓存中。这就是为什么即使是在共享内存区域——堆内存在多线程环境下也可能出现内存不可见性的问题。 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内存模型JMM是一种抽象概念它定义了Java程序中各种变量的访问规则尤其是针对堆内存中的共享变量。JMM确保了并发环境下的原子性、有序性和可见性 主内存与本地内存的关系根据JMM的描述所有共享变量都存储于主内存中每个线程具有自己的本地内存保存了该线程读写共享变量的副本。线程间的通信必须通过主内存进行即线程先将本地内存中的共享变量刷新回主内存然后其他线程从主内存读取最新值并更新到自己本地内存的过程。 内存操作的顺序保证JMM通过内存屏障来控制指令重排序从而确保特定操作的顺序性比如volatile变量的写后读操作不会被重排序。 内存可见性的实现JMM提供了一系列规则和机制来保证不同线程对共享变量修改的可见性例如volatile变量的写会强制刷入主内存并使其他线程对该变量的读失效进而从主内存重新加载。 综上所述Java内存模型在多线程编程中扮演着核心角色通过规范和约束线程如何访问和更新共享变量有效地解决了并发环境下的内存一致性问题。 Java内存模型与Java内存区域的关系 两者区别 Java内存模型JMM和Java运行时内存区域是两个不同的概念层次它们在描述并发编程的内存行为时有着各自的侧重点 Java内存模型JMM从抽象层面定义了线程之间如何通过主内存进行交互以及如何保证数据的一致性和有序性。JMM关注的是对共享变量访问规则的规范比如可见性、原子性和有序性它是一组关于程序中所有变量访问操作的协议或约定。 Java运行时内存区域这是更为具体的概念指Java虚拟机JVM在运行Java程序时实际划分的内存区域包括堆、栈、方法区、程序计数器等。这些内存区域分别存储着对象实例、局部变量、类信息、线程上下文等不同类型的内存数据并且各区域具有不同的生命周期和管理策略。 联系与映射 尽管JMM与Java运行时内存区域在概念上有所差异但它们之间存在着密切的联系和映射关系 主内存与共享数据区域 在JMM中主内存对应于Java运行时内存区域中的堆和方法区。堆存放了Java对象实例即多线程可以共享的对象数据而方法区则包含了类的元数据和静态变量等这些也是全局可访问的信息因此它们都属于“主内存”的范畴。 本地内存与私有数据区域 JMM中的本地内存实际上是一个抽象概念涵盖了缓存、写缓冲区、寄存器等硬件设施对应到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关键字的作用 在Java并发编程中volatile关键字是一个重要的工具它用于修饰共享变量确保了该变量在多线程环境下的可见性和禁止指令重排序。当一个线程修改了volatile变量的值时其他线程能够立即看到这个更新后的值这是因为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原则 为了更深入地理解并发控制机制Java内存模型还引入了内存屏障Memory Barrier的概念这是一种硬件级别的指令用于确保特定内存操作顺序并刷新缓存。Java编译器会根据JMM规则在适当的时机插入内存屏障以实现对volatile变量和其他同步原语的正确支持。 另外Java内存模型通过happens-before原则来简化程序员理解和推理程序行为。它定义了一系列先行发生关系比如程序次序规则、监视器锁规则等这些规则明确了事件之间的执行顺序如果A happens-before B那么线程A对共享变量的修改对于线程B来说一定可见。 例如 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变量是否已经被正确设置。 © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点赞、收藏、关注哦