1. 什么是JMM?
JMM描述了在源代码中定义的共享变量和底层计算机系统是如何处理这些变量的关系。
(1) 编译器可以在把源代码翻译成机器指令的时候,对指令做重新排序
(2) 因为计算机有缓存的存在,所以共享变量有可能是保存在缓存中的,那么会跟内存中的共享变量值不一致
(3) Cpu在执行指令的时候,可能会对指令从排序。典型的就是把变量值保存到主存中时,一般是延迟的。所以,在一个线程中对一个共享变量做的修改,可能在其他线程中是看不到的。
(4) JMM允许在没有使用Synchronization 和 visibility时候,编译器、cpu可以以任意的方式重排序指令只要在单线程环境下,对最终计算结果没有影响。
(5) JMM定义了线程和主内存之间的一种关系
· 每个线程都有一个工作内存(对缓存和寄存器的抽象),用来保存共享变量的副本
· 主内存中的共享变量和线程中工作内存中的副本交互的时候需要有些规则
①原子性,定义了哪些指令是不可分割的。
②可见性,定义了在什么样的条件下,写线程对共享变量写入的新值,对于读线程来说是可见的。
③有序性,定义了在什么样的条件下,多个线程执行指令是可以乱序的。
2. 什么是原子性
在线程中对主内存中共享变量的访问和修改,必须要保证原子性性。
(1) 原子性保证了,在线程中对共享变量的访问,要么读到的值是初始值,要么读到的值是由其他线程更新后的新值;不会说,一个线程读到另外一个线程更新一半的值。
(2) 原子性并不能保证,一个线程中读到的值是最新的值。
3. 什么是可见性
只有满足以下条件,才能保证可见性。
(1) 写线程释放了锁资源,随后读线程获取了锁资源。那么读线程读到的是写线程写入的最新值
(2) 共享变量是由volatile修饰的。在这个条件下,写线程在执行其他操作之间,会立即把值刷新到主存中去;读线程如果要读这个变量,那么必须要重新load这个变量,以便获取最新值。
(3) 如果一个线程第一次去读一个共享变量,那么他获取的值只有两种情况。要么是变量的初始值;要么值是其他线程写入后的值。读完后,缓存在这个线程的工作内存中
(4) 当一个线程结束的时候,它会把工作内存中共享变量的副本刷新到主存中去。
4. 有序性
有序性,需从线程内、线程之间两个方面来考虑。
(1) 在线程内,指令的执行相当于是像程序中写个那么是有序的。
(2) 如果在线程之间去观察,指令的执行的话,那么没有正确同步化的代码执行时就是乱序的。
5. 再解volatile变量
(1) Volatile变量不能保证原子性;比如定义count是volatile的,那么count++是个复合操作
(2) 有序性,可见性仅仅是针对这个特殊的volatile变量的。
· volatile基本类型,能保证可见性
· volatile引用类型,仅仅能保证引用的可见性;不能保证该引用指向的对象中的域对其他线程的可见性。
· volatile数组,仅仅能保证数组引用的可见性;不能保证数组中元素对其他对象的可见性。
这就限制了volatile变量的使用场景:volatile基本类型、volatile不可变类。
(3) volatile变量的使用场景
· volatile变量不会参与到对象的约束
· 对一个volatile变量的写,不会依赖于这个变量的最新值。
6. Synchronization 和内存visibility的语意
同步是利用对象的锁来达到的,Synchronization有两层意思:排斥性、内存可见性。
(1) 排斥性
线程进入一个同步块时,必须首先要得到一个锁。如果得不到锁,那么它就会阻塞;一旦一个线程获取了锁资源后,那么其他线程就不能进入受到同一个锁保护的代码块(互斥性),指定这个线程释放了锁后,其他线程才能进入同步块。
(2) 内存可见性
线程获得锁后,共享变量在这个线程的工作内存中的缓存就要失效,必须load主存中的值。
线程退出锁时,必须刷新内存。
7. 内存可见性
(1) 什么是内存可见性
一个写线程对共享变量的修改后,其他的读线程应该要看到这个共享变量的最新值。
(2) 什么原因导致在一个线程中对共享变量的修改,在另外的一个读线程中看不到
· Cpu缓存对共享变量的缓存
· 编译器在编辑源代码的时候,对机器指令的重新排序
· Cpu在执行指令的时候,对所执行的指令的重新排序
· 读线程中,因为有对共享变量的缓存存在,所以读到的过期数据
(3) 如何解决内存可见性
· 使用volatile变量,只能保证内存可见性;不能保证多个操作的原子性。
· 使用synchronized同步块。同步块能保证原子性及内存的可见性
· 使用final,在对象构建的时候。使得构建安全发布的共享对象。
8. Volatile变量的问题
(1) 老的JMM对volatile变量的语意。
· 设置内存栅栏,编译器和cpu不会volatile变量重新排序;可以理解为在源代码中怎么写的顺序,在执行的时候也是按照这个顺序来执行。也就是coder们能控制程序的语意。
· 在一个读线程中读volatile变量一定发生在另外一个写线程之后。
· 读线程中一定能读到volatile变量的最新值,因为线程对volatile变量的操作直接发生在主内存中。
· 以上几点一直在说volatile变量,那么当volatile变量和其他变量一起用呢?是不是也能阻止其他变量的重新排序?
MapconfigOptions;
char[]configText;
volatileboolean initialized = false;
以上是一些共享变量
// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// In Thread B
while (!initialized)
sleep();
// use configOptions
想法是在线程A中完成对configOptions的初始化工作,然后再线程A中设置initialized = true表示完成对configOptions的初始化。
线程B中先读initialized是不是true,如果是true表示configOptions完成初始化工作了,可以被正确使用。
这个一种对volatile变量的一种使用方法,但是由于老的JMM允许volatile变量可以和其他不是volatile变量重排序,所以在Thread B中可能会用到没有完成初始化工作的configOptions对象。所以在老的JMM下volatile变量基本上没啥用,在新的JMM下修正了这个问题。
(2) 修正后JMM
一个线程中读volatile变量一定是发生在另外一个线程中对volatile写后,并且这会影响到其他非volatile变量。
9. Happens before是什么?
新的JMM保证了在多线程环境下使用volatile 、synchronized不会重新排序机器指令。
· 对volatile变量的读一定发生在对volatile变量的写之后
· 线程对锁的获取一定发生在另外一个线程对锁的释放之后
· Thread.start()调用一定发生在某个线程中的指令被执行之前
10. 什么是数据竞争(data race)
(1) Data race是如何发生的
一个共享变量被多个线程读,并且至少有一个线程写的。并且读-写不是按happens-before关系建立的,程序就存在一种data race。
11. 初始化安全与final
只要对象是在构造器中安全构造的(安全构造,意味着在构造器中,不会发布this),那么其他线程中就能看到final域的最新值。不需要额外的Synchronization来保证内存的可见性。
(1) 基本变量的final域
· Static final
在编辑期间完成初始化工作,一旦完成后,就不能改变。
· 非static final
在构造器中完成初始化工作,一旦完成后在对象的整个生命周期内都不能改变
(2) 引用类型的final域
在构造器中完成初始化,一旦初始化工作完成,引用就不能指向其他对象。
(3) 实际上构造器中对final域的初始化工作,对于读线程来说,也就构建了类似一种happens-before关系,线程中读到的final域,一定是在构造器中对final域完成初始化之后的值。所以不需要同步来保证内存可见性。