JVM--内存模型与线程

一、硬件与效率的一致性

  计算机的存储设备与处理器的运算速度存在几个数量级的差距,现在计算机系统不得不在内存和处理器之间增加一层高速缓存(cache)来作为缓冲。将运算需要的数据复制到缓存中,让运算能够快速进行,当运算结束的时候再讲数据从缓存同步到内存中,这样处理器无须等待缓慢的内存读写。除了增加高速缓存外,为了使处理器的内存的运算单元能被充分的利用,处理器可能对输入的代码进行乱序执行优化,即常说的重排序,计算后对乱序执行的结果重组,保证结果与顺序执行代码的结果一致,但是并不保证各个语句的计算顺序与代码顺序一致。

  基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,引进了一个新的问题:缓存一致性。在多处理器计算机系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,当处理器的运算任务涉及到同一块主内存区域的时候,很可能导致各自的缓存数据不一致。解决这个问题,需要各个处理器访问缓存的时候遵循一些协议,在读写时根据协议来进行操作。

二、java内存模型

  内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象,不同架构的物理机器可以拥有不一样的内存模型,而java虚拟机有自己的内存模型。同样的,java虚拟机的即时编译器中有类似的指令重排序优化功能。java虚拟机规范中定义的内存模型用来屏蔽掉各种硬件环境和操作系统的内存访问差异,以实现让java程序在各个平台都能达到一致的内存访问效果。在jdk1.5发布后,java的内存模型已经成熟和完善起来。

  java内存模型的主要目标是为了定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。这里的变量与java语言中说的变量不是同一个东西,包括:实例字段,静态变量和构成数组对象的元素,但是不包括局部变量和方法参数(线程私有,不会共享,自然不存竞争问题)。java内存模型不限制执行引擎使用处理器的寄存器或缓存来和主内存进行交互,不限制即时编译器进行重排序优化。java内存模型的规范如下:

1.主内存与工作内存

Java内存模型规定
1.所有的变量都存储在主内存(虚拟机内存的一部分)。
2.每条线程有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的读取,赋值等必须在工作内存中进行。
3.不同线程之间无法直接访问对方工作内存中的变量,线程间的变量值传递需要借助主内存完成。

2.java内存间的交互操作

内存交互:即一个变量如何从主内存拷贝到工作内存,又如何从工作内存同步到主内存之类的实现细节。Java内存模型定义八种操作来完成,并且下面八种操作都是原子的,不可再分的。如下:

lock(锁定):作用于主内存中的变量,它把变量标识为一条线程独占的状态。

unlock(解锁):作用主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存中的变量,把一个变量从主内存传输到线程的工作内存中,以便随后的load操作。

load(载入):作用于工作内存中的变量,把read操作从主内存中得到的变量放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,把一个工作内存中的变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令的时候将会执行这个操作。

assign(赋值):作用工作内存的变量,把从执行引擎接收到的值付赋给工作内存中的变量。

store(存储):作用于工作内存中的变量,把工作内存中的一个变量的值传递到主内存中,以便后续的write操作。

write(写入):作用于主内存中的变量,把store操作得到的变量的写入到主内存中对应的变量中。

注意:java内存模型要求read和load操作顺序执行,同时要求store和write操作顺序执行。

3.volatile变量的特殊规则

  • 保证volatile变量对所有线程的可见性,与普通变量的区别保证变量的新值能立即同步到主内存,以及每次使用的时候立即从主内存刷新。
  • volatile变量禁止指令重排序优化(主要通过内存屏障指令来实现)

内存屏障:指重排序的时候,不能把后面的指令重排序到内存屏障之前的位置。

4.long和doubel型变量的特殊规则

允许虚拟机将没被volatile修饰的64位数据的读写划分为两次32位的操作来进行。如long和double变量。但是目前商用虚拟机几乎把64数据的读写操作作为原子操作来对待。

5.原子性、可见性和有序性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子代表不可分割性。基本数据类型的访问读写具备原子性。synchronized块之间的操作具备原子性。

可见性:当一个线程修改共享变量的值,其他线程能够立即知道这个修改。java中volatile,synchronized和final能够实现变量的可见性。

有序性:线程内表现为串行语义,指令重排序和工作内存与主内存同步延迟的现象导致在其他线程观察另外一个线程,其操作都是无序的。java提供volatile和synchronized两个关键字来保证线程之间操作的有序性。

5.先行发生原则

java语言中有一个先行发生的原则,即happen-before,是判断是否存在竞争和线程是否安全的主要依据。java内存模型规定天然先行发生关系:

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

三、java线程在虚拟机中的实现

  线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和调度分开,各个线程可以共享进程资源,又可以独立调度(线程是CPU调度的基本单位)。主流的操作系统都提供了线程实现,Java语言则提供在不同硬件和操作系统平台下对线程的统一处理。每个执行了start()且还未结束的java.lang.Thread类的实例就代表一个线程。

1.Java线程的实现

Java线程模型在jdk1.2后是基于操作系统原生线程模型来实现的,因此操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的。对于sun JDK来说,它在windows和linux都是使用一对一的线程模型实现,即一条java线程映射到一条轻量级进程之中。

2.Java线程调度

线程调度:系统为线程分配处理器使用权的过程。主要两种方式:协同式线程调度和抢占式线程调度。java线程调度取决于操作系统的线程调度模式。

3.线程状态转换

java语言定义五种线程状态,任意一个时间点,一个线程只能有且只有其中一种状态,五种状态如下:

1. 新建状态(New)         : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked)  : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 结束状态(Terminated): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程状态转换关系如下:

原文地址:https://www.cnblogs.com/liupiao/p/9425161.html