Java内存模型详解:JMM, Happens-Before原则及其应用
Java内存模型(JMM,Java Memory Model)是Java虚拟机规范的一部分,它定义了Java程序中各种变量(线程共享的实例字段、静态字段和数组)的访问规则。这种规定为Java提供了一种在并发编程环境中的内存可见性保证,避免了因为多线程之间的数据不同步而导致的各种难以排查的问题。
1. JMM的基本概念
在深入理解Java内存模型之前,首先需要了解以下几个核心概念:
- 主内存与工作内存:在JMM中,所有实例字段、静态字段和数组都存储在主内存中,它是一个共享的资源。每个线程都有自己的工作内存,工作内存存储了被该线程使用到的变量的主内存副本拷贝。
- 内存操作:JMM定义了8种原子操作来直接与主内存交互,但这里为了简单化,我们只关心以下几种:read(读取主内存变量到工作内存)、load(工作内存变量就绪)、use(线程使用工作内存变量)、assign(线程给工作内存变量赋值)、store(将工作内存变量的值写入主内存)、write(将store的值覆盖主内存的值)。
2. Happens-Before原则
为了保证内存的可见性、有序性以及原子性,JMM引入了原则。这是一种保证两个操作之间的先后顺序的原则。如果一个操作另一个操作,那么第一个操作的结果对第二个操作是可见的。
以下是几个基本的规则:
- 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作happens-before于书写在后面的操作。
- 锁的规则:解锁操作happens-before于后续对同一个锁的加锁操作。
- volatile变量规则:对一个volatile字段的写操作happens-before于后续对该volatile字段的读操作。
- 线程启动规则:Thread对象的start()方法调用happens-before于此线程的每一个操作。
- 线程终止规则:线程中的所有操作都happens-before于其他线程检测到此线程已经终止。
以下是一个简单的代码例子,用于展示的规则:
在上面的代码中,方法中的写操作于方法中的读操作。这意味着,如果一个线程调用了方法并更新了的值,随后另一个线程调用方法,那么第二个线程会看到的新值。
注意: volatile关键字并不能保证复合操作的原子性,它只能保证单一读/写操作的可见性。
至此,我们已经对JMM和原则有了一个初步的了解。
3. 内存可见性和指令重排序
为了提高程序的执行性能,处理器、编译器或者JVM在不改变单线程程序语义的前提下,可能会对输入代码进行优化,这导致了两个问题:内存可见性和指令重排序。
内存可见性
当多个线程访问共享变量时,一个线程修改了这个变量的值,其他线程可能立即看不到这个修改。这就是内存可见性问题。
例如:
理论上,应该输出,但由于内存可见性问题,它可能输出,或者这行代码根本不会执行。
使用关键字可以保证变量的可见性,因为它遵循规则。
指令重排序
指令重排序是指CPU为了提高程序运行效率,对代码进行排序优化,但可能会导致程序并行执行时出现数据不一致的情况。
考虑以下代码:
假设和分别在两个不同的线程中执行,我们期望输出的值为。但由于指令重排序,语句1和语句2的执行顺序可能会互换,这样在中,的值可能还是。
使用关键字或关键字均可以避免指令重排序的问题。
4. 和
这两个关键字都可以用于确保多线程间的内存可见性。
- volatile: 仅保证变量的读写操作不会被重排序,并且保证变量的写操作对其他线程是可见的。但它不能保证复合操作的原子性。
- synchronized: 它既可以确保块内的操作是原子性的,又可以确保块内的内存操作对其他线程是可见的。
例子:
在这个例子中,方法和方法都被关键字修饰,确保了的读写操作不仅是原子性的,而且对其他线程是可见的。
5. JMM的总结
JMM为Java提供了一个在并发编程环境中的内存可见性、有序性以及原子性的保证。通过理解其核心概念和使用相应的关键字,我们可以更好地编写并发程序。
6. Java并发工具
Java为开发者提供了一个非常强大的并发工具库,即包。这个包中包含了许多类和接口,它们可以帮助开发者更容易地写出高效、健壮和可扩展的并发应用。
java.util.concurrent.atomic
这个子包包含了一些原子类,它们利用了CPU的底层指令,实现了高效的原子操作。以下是一些常用的原子类:
例如,提供了一个方法,该方法原子地增加整数的值并返回增加后的值。
这些原子操作都是线程安全的,并且它们的性能通常比关键字更好。
java.util.concurrent.locks
有时候,您可能需要比关键字更细粒度的锁定。在这种情况下,您可以使用包中的。
这个锁还提供了其他高级特性,例如条件变量、锁公平性和锁超时。
java.util.concurrent
这个包还包含了其他的并发工具,例如:
- 和 :用于并发任务的执行。
- 和 :代表异步计算的结果。
- 、 和 :用于线程之间的同步。
以下是一个使用和的简单示例:
这个示例中,我们创建了一个固定大小的线程池,并提交了一个简单的任务。会等待任务完成并返回结果。
总结
通过结合Java内存模型(JMM)的知识和包中的工具,Java开发者可以编写出高效、健壮和可扩展的并发应用。确保您熟悉了上面提到的各种工具和它们的用途,这样您在编写并发代码时可以做出明智的决策。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/14932.html