【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
1. Java 内存内存模型 vs JVM 运行时数据区
2. 初看 Java 内存模型
- 多线程程序语义:当多个线程修改了共享内存中的值时,应该读取到哪个值的规则。这些语义没有规定如何执行多线程程序,相反,他们描述了允许多线程程序的合法行为。
- 说白了,Java 内存模型实际上描述的是 Java 语言在执行多线程程序时的一些规则。
3. 多线程中的问题
- 所见非所得
- 无法肉眼去检测程序的准确性
- 不同的运行平台有不同的表现
- 错误很难重现
public class Demo10 {
int i = 0;
boolean isRunning = true;
public static void main(String[] args) throws InterruptedException {
Demo10 demo = new Demo10();
new Thread(() -> {
System.out.println("here i am ...");
while (demo.isRunning) {
demo.i++;
}
System.out.println(demo.i);
}).start();
Thread.sleep(3000);
demo.isRunning = false;
System.out.println("shutdown ...");
}
}
- 可以通过配置指定 VM options
<img src="https://oscimg.oschina.net/oscnet/up-2a07b0064afd2e83b0df8f53c6c48cd6719.png" width="700"/>
参数 | 32 位 JDK | 64 位 JDK |
---|---|---|
-server | 不打印i的值 | 不打印i的值 |
-client | 打印i的值 | 不打印i的值 |
4. 工作内存缓存
<img src="https://oscimg.oschina.net/oscnet/up-ba96a2acfd74d39c2bc220649f8c8d360b0.png" width="700"/>
- data 数据在两个线程间的同步问题。
5. JIT 编译(Just In Time Compiler)
1. 脚本语言和编译语言的区别
- 解释执行:即咱们说的脚本,在执行时,有语言和解释器将其一条条翻译成机器可识别的指令。
- 编译执行:将我们编写的程序,直接编译成机器可以识别的指令码。
2. Java 是脚本语言还是编译语言?
- Java 介于脚本语言与编译语言之间。
3. 针对上面程序的问题解决办法
isRunning
添加volatile
关键字即可。
volatile boolean isRunning = true;
6. volatile 关键字
- 可见性问题:让一个线程对共享变量的修改,能够及时地被其他线程看到。
- Java内存模型规定:对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步。
volatile 关键字功能
- 禁止缓存;
- volatile 变量的访问控制符会加个 ACC_VOLATILE
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
- 对 volatile 变量相关的指令不做重排序。
7. Shared Variables 定义
- 可以在线程之间共享的内存称为共享内存或堆内存。
- 所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享变量。
- 冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
- 这些能被多个线程访问的共享变量是内存模型规范的对象。
- 定义在:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1
8. 线程间操作的定义
- 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。
- Java 内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。
线程间操作有:
- read 操作(一般读,即非 volatile 读)
- write 操作(一般写,即非 volatile 写)
- volatile read
- volatile write
- Lock.(锁 monitor)、Unlock
- 线程的第一个和最后一个操作
- 外部操作
9. 对于同步的规则定义
- 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步。
- 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步。
- 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步。
- 启动线程的操作与线程中的第一个操作同步。
- 线程T2的最后操作与线程T1发现T2已经结束同步。(isAlive,join可以判断线程是否终结)
- 如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步。通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted。
10. Happens-before 先行发生原则
-
happens-before 关系用于描述两个有冲突的动作之间的顺序,如果一个 action happens before 另一个 action,则第一个操作被第二个操作可见,JVM 需要实现如下 happens before 规则:
- 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
- 某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作。
- 在某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
- 在某个线程对象上调用 start() 方法 happens-before 被启动线程中的任意动作。
- 如果在线程 t1 中成功执行了 t2.join(),则 t2 中的所有操作对 t1 可见。
- 如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,则有 a happens-before c。
-
当程序包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据竞争。
-
遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存。
11. final 在 JMM 中的处理
-
final 在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的 final 字段的正确构造版本。
- 伪代码示例:
f=new finalDemo();
读取到的 f.x 一定最新,x 为 final 字段。
- 伪代码示例:
-
如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则他将在看到默认值。
- 伪代码示例:
public finalDemo() {x=1;y=x;};
y会等于1。
- 伪代码示例:
-
读取该共享对象的final成员变量之前,先要读取共享对象。
- 伪代码示例:
r=new ReferenceObj(); k=r.f;
这两个操作不能重排序。
- 伪代码示例:
-
通常被static final修饰的字段,不能被修改。然而
System.in
、System.out
、System.err
被static final修饰,却可以修改,遗留问题,必须允许通过set方法修改,我们将这些字段称为写保护,以区别于普通的final字段。<img src="https://oscimg.oschina.net/oscnet/up-f133a960110d3ad875503e282867080e111.png" width="400"/>
<img src="https://oscimg.oschina.net/oscnet/up-72d5ed20299b069c63f84ef9edb43825384.png" width="400"/>
12. Word Tearing字节处理
- 有些处理器(尤其是早起的Alphas处理器)没有提供写单个字节的功能。在这样的处理器上更新byte数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。
- 这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就需要寻求其他方式来解决问题。
- 因此,编程人员需要注意,尽量不要对 byte[] 中的元素进行重新赋值,更不要在多线程程序中这样做。
13. double 和 long 的特殊处理
-
由于《Java语言规范》的原因,对非 volatile 的 double、long 的单次写操作是分两次来进行的,每次操作其中 32 位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。
<img src="https://oscimg.oschina.net/oscnet/up-133fd652edb772e142e17713a7a33dfdec1.png" width="300"/>
-
商业JVM不会存在该问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。
-
读写volatile修饰的long、double是原子性的。
-
《Java语言规范》中说道:建议程序员将共享的64位值(long、double)用volatile修饰或正确同步其程序以避免可能的复杂的情况。
来源:oschina
链接:https://my.oschina.net/shadowolf/blog/3147133