你好,我是风一样的树懒,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。
文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读
JMM(Java Memory Model,Java内存模型) 是 Java 并发编程中非常核心的一个概念,它定义了 Java 程序中不同线程之间如何共享内存以及如何保证内存的可见性、原子性和有序性。JMM 的设计目标是为 Java 提供一个规范,确保在多线程环境下执行时,程序的行为符合预期,而不依赖于底层硬件架构的具体实现。
可见性:当一个线程修改了共享变量的值,其他线程能立刻看到这个修改。
原子性:一个操作在执行时不受其他线程的干扰,要么全部执行完,要么完全不执行。
有序性:程序代码的执行顺序应当符合代码中的顺序,即程序中按顺序写的操作应该按顺序执行。
JMM 的设计背后有几个关键概念,它们解决了多线程编程中的常见问题。
主内存(Main Memory):所有线程共享的内存区域,所有的实例字段、静态字段和数组元素都存储在主内存中。
工作内存(Working Memory):每个线程都拥有自己的工作内存,工作内存是线程对主内存的副本,线程的操作是通过工作内存来访问主内存中的数据。
当线程执行时,它会将主内存中的数据加载到工作内存中,线程通过工作内存来读取和修改数据,但修改后的数据并不会立即写回主内存,直到某个操作被强制刷新。
可见性问题是指当多个线程修改共享变量时,一个线程的修改在其他线程中不可见。JMM 提供了一些规则来保证线程之间的可见性:
线程间共享变量的更新机制:通过 volatile 关键字和 synchronized 关键字,JMM 可以确保一个线程修改的变量对于其他线程是可见的。
volatile:当一个变量声明为 volatile 时,JMM 保证对该变量的写操作对其他线程立即可见。同时,JMM 也保证了该变量的读取操作是从主内存中读取,而不是从工作内存中读取。
synchronized:synchronized 保证了互斥访问,同时,它也具有可见性,线程在进入 synchronized 方法之前,会把工作内存中的数据刷新到主内存中,线程离开 synchronized 方法时,修改的数据会被写回主内存。
原子性指的是一个操作不可分割,要么执行完成,要么完全不执行。在多线程环境下,原子性是非常重要的,JMM 保证了基本数据类型的操作是原子的,但并不保证对复合操作(如 i++)的原子性。
volatile 对单一变量的读写操作保证原子性,但对于复合操作(如累加、加法等),volatile 无法保证原子性。
synchronized 关键字可以保证一个代码块中的操作是原子的,多个线程对同一个 synchronized 代码块的访问会互斥,避免并发冲突。
有序性问题是指程序的执行顺序不一定按照代码的顺序执行。在并发环境中,编译器和处理器可能会进行指令重排序,以提高程序的执行效率。然而,这可能会导致一些问题,尤其是在多线程环境下,线程的执行顺序不能保证。
为了避免这种情况,JMM 通过 happens-before 规则 来规定操作的执行顺序,确保程序的执行顺序符合预期。
synchronized:进入某个 synchronized 区块之前的操作,必须发生在离开该区块之后的操作之前。也就是说,进入和离开 synchronized 区块有明确的执行顺序。
volatile:对 volatile 变量的写操作,发生在后续对该变量的读操作之前。也就是说,写操作和读操作之间也有执行顺序保证。
为了保证有序性和正确性,JMM 定义了一系列的 happens-before 规则,确保在多线程环境下,程序的执行顺序是符合预期的。以下是几个常见的 happens-before 规则:
程序顺序规则:在一个线程内,按照代码的顺序执行,前面的操作 happens-before 后面的操作。
监视器锁规则:一个线程释放锁 happens-before 另一个线程获取同一个锁。
volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。
传递性规则:如果 A happens-before B,B happens-before C,那么 A happens-before C。
volatile 关键字是 JMM 提供的用于处理可见性问题的机制。它的作用不仅仅是保证变量的可见性,还涉及到有序性。
可见性:当一个线程修改了 volatile 变量的值,其他线程可以立即看到修改的结果。
禁止指令重排序:volatile 变量的读写操作会被禁止重排序,这意味着所有对 volatile 变量的操作会按照代码中出现的顺序执行,保证了有序性。
然而,volatile 不能保证原子性。对于复合操作,仍然需要使用 synchronized 来保证线程安全。
在底层实现上,JMM 主要通过以下方式来保证多线程的正确性:
缓存一致性协议:如 MESI 协议(修改、独享、共享、无效)来保证多处理器系统中的缓存一致性。它确保了各个核心之间的缓存是同步的。
内存屏障(Memory Barrier):内存屏障(如 StoreLoad、LoadLoad、StoreStore)是硬件级别的一种机制,它通过禁止指令重排序来保证一定的执行顺序。
指令重排序:由于 JMM 的存在,编译器和处理器会为了优化性能而对指令进行重排序,可能导致意想不到的结果。例如,A线程修改了某个共享变量,而B线程读取时却看不到修改的结果,这就是由于指令重排序所造成的问题。
缓存一致性问题:由于每个线程有自己的工作内存,多个线程同时访问同一变量时可能会遇到缓存一致性问题。
JMM 为 Java 并发编程提供了基本的内存语义,Java 的并发包(如 java.util.concurrent)则通过高层的抽象(如 Executor、CountDownLatch、Semaphore 等)帮助开发者更方便地使用并发编程模型,同时保证线程间的正确性和效率。
今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!