[关闭]
@huangyichun 2017-08-30T15:46:43.000000Z 字数 9438 阅读 903

Java内存模型与多线程总结

多线程


Java内存模型

关于并发编程

线程之间的通信

线程之间的同步

Java的并发采用的是共享内存模型

Java内存模型

image.png-17.7kB

image.png-13.7kB

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。


上面也说到了,Java内存模型只是一个抽象概念,那么它在Java中具体是怎么工作的呢?为了更好的理解上Java内存模型工作方式,下面就JVM对Java内存模型的实现、硬件内存模型及它们之间的桥接做详细介绍。

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图:
image.png-6.2kB

下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区:
image.png-20.5kB

下图展示了上面描述的过程:

image.png-22kB


硬件内存架构

不管是什么内存模型,最终还是运行在计算机硬件上的,所以我们有必要了解计算机硬件内存架构,下图就简单描述了当代计算机硬件内存架构:

image.png-14.4kB

Java内存模型和硬件架构之间的桥接

正如上面讲到的,Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆,从硬件上看,不管是栈还是堆,大部分数据都会存到主存中,当然一部分栈和堆的数据也有可能会存到CPU寄存器中,如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:

image.png-29.2kB

当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:

  1. 1. 共享对象对各个线程的可见性
  2. 2. 共享对象的竞争现象

共享对象的可见性

下图展示了上面描述的过程。左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:

image.png-18.4kB

要解决共享对象可见性这个问题,我们可以使用java volatile关键字。 Java’s volatile keyword. volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的,后面会讲到。

竞争现象

image.png-17.8kB

要解决上面的问题我们可以使用java synchronized代码块。synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。

支撑Java内存模型的基础原理

指令重排序

在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。但是,JMM确保在不同的编译器和不同的处理器平台之上,通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

  1. 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

内存屏障(Memory Barrier )

上面讲到了,通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:

  1. 编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
  2. Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。

这和java有什么关系?上面java内存模型中讲到的volatile是基于Memory Barrier实现的。

如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。

  2. 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

happens-before

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

与程序员密切相关的happens-before规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。

  2. 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。

  3. volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

  4. 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。


线程状态图

image.png-100.5kB
说明:
线程共包括以下5种状态。
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程wait()时,执行interrupt()方法

  1. /**
  2. * wait()和notify()方法需要在synchronized方法或者代码块中调用
  3. * 在调用wait方法时,线程从运行状态转换成阻塞状态,并且释放锁。
  4. * 在线程处于wait()阻塞状态下,线程调用Interrupt()方法时,线程从等待阻塞状态,转换为
  5. * 同步阻塞(即线程等待synchronized锁),同时抛出异常,程序继续执行。
  6. */
  7. public class TestWait {
  8. public static void main(String[] args) {
  9. TestWait testWait = new TestWait();
  10. Thread thread = new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. testWait.waitTest();
  14. }
  15. });
  16. thread.start();
  17. try {
  18. Thread.sleep(2000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. thread.interrupt();
  23. System.out.println("运行结束");
  24. }
  25. public synchronized void waitTest(){
  26. try {
  27. wait();
  28. } catch (InterruptedException e) {
  29. System.out.println("发生中断, 释放wait()锁");
  30. }
  31. System.out.println("wait the synchronized");
  32. }
  33. }

interrupt(), interrupted()和isInterrupted()方法

interrupt()方法: 作用是中断线程。

interrupted()方法

判断的是当前线程是否处于中断状态。是类的静态方法,同时会清除线程的中断状态。

  1. public static boolean interrupted() {
  2. return currentThread().isInterrupted(true);
  3. }

isInterrupted()方法

判断调用线程是否处于中断状态
例如:

  1. public static void main(String[] args){
  2. Thread thread = new Thread(()->{}); //定义一个线程,伪代码没有具体实现
  3. thread.isInterrupted();//判断thread是否处于中断状态,而不是主线程是否处于中断状态
  4. Thread.isInterrupted();判断主线程是否处于中断状态
  5. }

线程停止

  1. @Override
  2. public void run() {
  3. while (!isInterrupted()) {
  4. // 执行任务...
  5. }
  6. }
说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。

注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

  1. private volatile boolean flag= true;
  2. protected void stopTask() {
  3. flag = false;
  4. }
  5. @Override
  6. public void run() {
  7. while (flag) {
  8. // 执行任务...
  9. }
  10. }

说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。

综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:

  1. @Override
  2. public void run() {
  3. try {
  4. // 1. isInterrupted()保证,只要中断标记为true就终止线程。
  5. while (!isInterrupted()) {
  6. // 执行任务...
  7. }
  8. } catch (InterruptedException ie) {
  9. // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
  10. }
  11. }

线程方法总结:

Volatile的应用

synchronized

java中的每一个对象都可以作为锁,具体表现为以下三种形式:

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位

IMG_20170514_134609.jpg-1359.9kB
锁从低到高分为4中状态:
   无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

偏向锁

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注