Java 多线程与并发离不开 Java内存模型,但网上的博文我有点不能理解,这次看完 Java多并发实战和相关视频,做一份简单的总结。
内容大概分为下面几块。
它与JVM的内存模型不一样,JVM的内存模型是物理的内存区域划分。而 Java内存模型(即Java Memory Model,简称 JMM)是一种对虚拟机的一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM的主内存 存放的是线程公有的共享数据
JMM的工作内存 是线程私有的数据副本
线程中的变量如果是被Volatile修饰,或者是类似static修饰的共享变量,当数据被线程的工作内存中修改后,这个时候写回主内存,并且强制其他线程来重新读取该修改的变量。
可能有人会说,如果线程很多的话,那这种数据回写后强制通知的机制岂不是很费时吗?当时我也是这么认为的,直到我了解到 实际上计算机跑的线程数并不多,往往一个 6核的CPU并发处理 6个线程,通常分配的每个核1到3个线程,当然这不是绝对的,要看应用程序使用什么调度策略。但可以看出实际线程数并不多,通知开销没有想象的这么大。
题外话,如果对线程池有一定了解,那么会知道,我们是不用系统提供的3种策略的线程池的,而是定制 线程池,设置最大的线程数为你CPU的逻辑核数 + 1,同时并发的线程数在Java生成中也是不多的。
Java 内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。
JMM 为程序中的所有操作定义了一个偏序关系,称之为 Happens-Before
不管是多线程,还是单线程,如果要保证 执行操作B的线程看到操作A的结果,那么 A和B之间必须满足 Happens-Before关系。如果两个操作之间缺乏Happen-Before关系,那么JVM可以对它们任意地重排序。
这些规则不推荐大家强记,个人认为学习相关的线程知识点,这些规则好像就是理所应当的,不然无法满足 我们开发中遇见的一些 串行一致性的现象,就是按照多线程的规则书写,就表现的像单线程一样的正确运行。
例如我们对一个资源类的方法加synchronized 重量锁,那么线程A拿到锁在方法中的操作再解锁的数据变化是对下一个拿到这把锁的线程是可见的,这就是监视器规则,要始终理解到 只有满足 Happens-before 的规则才能满足执行操作B的线程看到操作A的结果,这就是 Happens-before的强大之处。
首先需要对指令重排做一段阐述。
对于程序执行的操作,在JVM编译的过程中,为了优化指令,重新排列的指令的执行顺序,指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。
当然重排序在单线程中是不能影响数据的依赖性和最终结果的,怎么理解?
i = 1; // 语句1 x = i; // 语句2 y = 2; //语句3
这里的话,语句2 就不能出现在 语句1前面,会影响数据依赖性,而 语句3可以出现在语句1前,也可以出现在 语句2前。
但是在多线程中就有 包含共享变量操作的语句重排序,由于内存可见性,就有可能会造成数据不一致的问题。
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
System.out.println(“(” + x + “,” + y + “)”);
}
这段代码的运行结果可能为(1,0)、(0,1)或(1,1),因为线程one可以在线程two开始之前就执行完了,也有可能反之,甚至有可能二者的指令是同时或交替执行的。
然而,这段代码的执行结果也可能是(0,0). 因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。得到(0,0)结果的语句执行过程,如下图所示。值得注意的是,a=1和x=b这两个语句的赋值操作的顺序被颠倒了,或者说,发生了指令“重排序”(reordering)。
而Volatile作为轻量级的同步机制,可以禁止指令重排序,所以Java源码中JUC包下有很多变量都使用了 Volatile来修饰。
个人在理解JMM相关点,往往不能一下子理解明白,是通过学习JUC的其他知识点再进行联想的才慢慢算是了解一点这个机制,不能保证写的没有问题,有误的话,还请大家指正。