????????Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
保证可见性:确保一个线程对共享变量的修改对其他线程是可见的。即当一个线程修改了某个共享变量时,其他线程能够立即看到这个变化。
保证原子性:确保一些特定操作不会被中断,使得它们在多线程环境中的执行结果与在单线程环境中的执行结果一致。例如,一个操作要么完整地执行,要么不执行,不会出现部分执行的情况。
保证有序性:规定程序中的代码执行顺序,禁止编译器和处理器对代码进行重排序优化。
内存可见性:当一个线程修改了共享变量的值,其他线程能够立即看到这个变化。
指令重排序:JMM允许编译器和处理器对指令进行重排序,但是在多线程环境下保证最终的执行结果与串行一致性执行的结果一致。
happens-before:提供了一种 happens-before 关系,即前一个操作的结果对于后一个操作是可见的。这种顺序性规则用于确保正确的内存可见性。
并发编程挑战:并发编程中常见的挑战包括竞态条件(Race Conditions)、死锁(Deadlocks)、内存可见性问题等。多线程并发访问共享资源时,会存在数据不一致、线程安全性等问题。
JMM关联:JMM定义了Java程序中多线程并发访问共享内存的规范。它规定了共享变量的可见性、有序性、原子性等特性,从而解决了并发编程中的一些问题。JMM提供了保证多线程间共享数据正确访问的规则和约束。
????????Java内存模型(JMM)中的主内存和工作内存是为了描述多线程并发访问共享变量时的内存交互情况而定义的两个概念。
?
????????JMM规定了主内存和工作内存之间的交互操作,主要涉及两种操作:
读取操作:当线程需要使用共享变量的值时,它会先从主内存中将变量的值复制到自己的工作内存中,然后执行操作。
写入操作:当线程修改了共享变量的值后,它会先在自己的工作内存中修改这个变量,然后将变更同步到主内存中。
????????主内存与工作内存的概念实现了线程间的通信机制,保证了多线程之间对共享变量的可见性、有序性和原子性操作。JMM规定了线程如何与主内存进行交互,从而保证了并发编程的正确性和稳定性。
?下面用一张图来看下JVM整体结构及内存模型
????????Happens-Before(先行发生,以下简称HB)是Java内存模型(JMM)中的一种规则或原则,用于确保多线程程序中操作的顺序性。它描述了在多线程环境中,对共享变量的操作顺序规则,从而确保正确的内存可见性和顺序性。
????????Happens-Before原则规定了一系列操作之间的先后顺序关系。如果一个操作Happens-Before于另一个操作,那么前一个操作对于后一个操作的结果是可见的。
程序顺序规则(Program Order Rule):在一个线程内,按照程序代码的先后顺序执行的操作,会产生Happens-Before的关系。
锁定规则(Monitor Lock Rule):对于一个锁的解锁操作Happens-Before于后续对于该锁的加锁操作。
volatile变量规则:对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。
传递性规则(Transitivity):如果操作A Happens-Before操作B,操作B Happens-Before操作C,则操作A Happens-Before操作C。
线程启动规则(Thread Start Rule):线程的启动操作Happens-Before于新线程的所有操作。
线程终止规则(Thread Termination Rule):线程的所有操作Happens-Before于其他线程检测到该线程终止。
保证顺序性:Happens-Before规则保证了对共享变量的写操作先于对该变量的读操作,保证了操作的顺序性。
保证可见性:由于Happens-Before关系可以保证操作的先后顺序,因此一个线程对变量的修改操作对于其他线程来说是可见的。
内存同步:Happens-Before规则提供了内存同步的机制,保证了多线程并发操作时内存数据的正确性和一致性。
Happens-Before原则是Java并发编程中重要的理论基础,它确保了多线程环境下对共享变量的操作顺序和可见性,帮助程序员编写正确、可靠的多线程并发程序。
volatile
是Java关键字,用于声明变量。它具有以下作用和特性:
volatile
关键字修饰时,对该变量的写操作会立即刷新到主内存,并且对该变量的读操作也会直接从主内存中读取,而不是从线程的工作内存中读取。volatile
关键字可以禁止编译器和处理器对被修饰变量的读写操作进行重排序优化。volatile
变量的写操作不会被重排序到其他操作之后,也不会将其之前的操作重排序到它之后。volatile
变量的读操作也不会被重排序到它之前的其他操作之后。volatile
适用于对变量的写操作不依赖变量的当前值,或者仅用于单一线程写、多线程读的情况。volatile
并不能保证原子性操作,因此对于多线程并发修改同一变量时可能需要额外的同步控制。状态标志位:在多线程中用作标识位,例如线程之间的控制标志位,确保不同线程之间状态的可见性。
单一写、多线程读:一个变量被多个线程读取但只有一个线程写入,可以使用volatile
来保证可见性。
Double-checked locking:在单例模式的双重检查锁定中,如果要求该单例实例对所有线程是可见的,可以将单例实例声明为volatile
。
当需要保证对一个变量的修改对其他线程是立即可见的,并且变量的写操作不依赖于当前值时,可以考虑使用volatile
关键字。
如果多个线程共享一个变量,其中一个线程修改了该变量,需要确保其他线程能够及时看到该变量修改后的值时,可以选择使用volatile
。
不保证原子性:volatile
只能保证可见性,不能保证操作的原子性。对于复合操作(例如i++
),需要额外的同步控制才能确保原子性。
不适用于线程安全的计数器:如果需要在多线程环境下进行自增等操作,应该使用Atomic
类提供的原子操作而不是volatile
。
不保证操作的有序性:虽然volatile
可以防止指令重排序,但并不能保证复合操作的有序性。
并不能替代锁:volatile
不能替代锁来确保原子性和线程安全,它适用于一些特定场景下的轻量级同步需求。
谨慎使用:volatile
的使用需要谨慎,不当的使用可能导致意想不到的结果,特别是对于复合操作和线程安全性的要求。
????????通过对Java内存模型(JMM)的理解,可以优化并发程序,提高性能和可靠性。以下是一些常见的性能优化技巧和最佳实践:
粒度控制:减小锁的粒度,使得锁的持有时间尽可能短,避免长时间的锁竞争。
分离锁:如果不同的数据可以使用不同的锁,可以将锁分离以减少不同数据间的锁竞争。
局部变量优于共享变量:尽可能使用线程本地的局部变量,减少对共享变量的访问。
CAS(比较并交换):使用CAS替代锁,例如Atomic
类提供的原子操作。
线程池调优:根据实际场景调整线程池的大小,避免线程数量过多或过少导致的资源浪费或性能瓶颈。
避免不必要的同步:只对需要保护的共享数据进行同步,避免过度同步。
缓存策略:合理选择缓存数据的策略,避免缓存过期导致的大量并发请求。
使用并发容器:使用ConcurrentHashMap
、ConcurrentLinkedQueue
等并发数据结构,避免手动加锁。
对象池:复用对象,减少对象的创建和销毁,降低内存开销。
减少内存泄漏:确保不再使用的对象能够被垃圾回收,避免内存泄漏问题。
压力测试:对并发程序进行压力测试,发现并发瓶颈,并针对性地优化。
性能监控:使用工具监控程序的性能指标,定位性能瓶颈,例如使用VisualVM、Arthas等进行性能分析。
????????Java内存模型(JMM)在Java并发编程中具有重要性,它定义了多线程并发访问共享内存时的规范,为开发高性能、可靠性的并发程序提供了必要的保证和指导。最后我们再总结一下Java内存模型的作用