Java 内存模型与线程

when ? why ? how ? what ?

计算机的运行速度和它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O 、网络通信或者数据库访问上。如何把处理器的运算能力“压榨”出来?

如何充分利用计算机处理器? 因为绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果这个 I/O 操作是很难消除的。又因为存储设备和处理器运算速度有几个数量级差距,所以在内存和处理器之间加了个高速缓存(这样处理器就无须等待缓慢的内存读写)。

每个处理器都有自己的高速缓存如何做到缓存的一致性?

如果处理器 A ,和处理器 B 都通过私有的高速缓存将处理后的数据存储到主内存中,如果在主内存中这需要存储的数据是同一个变量那应该以哪个数据为准呢? 所以出现了缓存一致协议。


JMM

Java内存模型(Java Memory Model,JMM)定义程序中各个变量的访问规则即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。变量指的是实例字段、静态字段和构成数组对象的元素(不包括局部变量与方法参数,后者是线程私有的)。

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,用户存储线程私有的数据。

Java 内存模型中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程可以访问,线程的工作内存保存了被改线程使用到变量的主内存副本拷贝,线程对变量的操作(读取赋值等)必须在工作内存中进行,不能直接读写主内存中的变量。

需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开。

8种操作来完成内存间交互操作

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

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

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

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

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

assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

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

write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

1、不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。

2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。

8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。


原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

分析以下哪些操作是原子性操作:

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

只有语句 1 是原子性操作。

语句 1 线程执行这个语句会直接将数据 10 写入工作内存中。

语句 2 先要度读取 x 的值,然后将 x 写入工作内存,虽然这 2 个操作都是原子性操作,但是合起来就不是。

语句 3 和语句 4 读取 x 的值,进行加 1 操作,写入新的值。

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

可见性指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java提供了volatile关键字来保证可见性。volatile 保证了新值能立即同步到主内存,以及每次用前立即从主内存中刷新。 synchronized 和 final也能实现可见性。

有序性

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生与书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  2. 管程锁定规则(Monitor Lock Rule):一个 unlock操作发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而“后面”同样是指时间上的先后顺序。
  3. volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个一个动作
  5. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  6. 线程终结规则(Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于他的finalize()方法的开始
  8. 传递规则(Transitivity):如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

volatile

  1. 保证可见性
  2. 不保证原子性
  3. 保证有序性(禁止指令重排优化)

volatile具有可见性

public static void main(String[] args) throws InterruptedException {
    ThreadOne threadOne=new ThreadOne();
    threadOne.start();
    Thread.sleep(1000);
    ThreadOne.stop=true;

}
}

class ThreadOne extends Thread{
public static volatile boolean stop=false;
@Override
public  void run(){
    while (stop==false){
        System.out.println(Thread.currentThread().getName()+" is running");
    }
}
}

结果:

如果将 volatile 关键字去掉会不会变成 死循环呢?

答案不会,因为锁粗化

原则上我们在编写代码的时候,总是推荐同步块的作用范围限制得尽量小————只在共享数据的实际作域中才进行同步,这样是为了使用需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作对同一个对象锁,将会把锁同步范围扩展(粗化)到整个操作序列的外部。

–深入理解JVM,13章线程安全与锁优化

下面是println方法的源码,使用了synchronized块
/**
 * Prints a String and then terminate the line.  This method behaves as
 * though it invokes <code>{@link #print(String)}</code> and then
 * <code>{@link #println()}</code>.
 *
 * @param x  The <code>String</code> to be printed.
 */
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

会变成

synchronized(this){
while (stop==false){
}
}

synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

volatile不保证原子性

public class VolatileTest {
public static volatile int race = 0;

public static void increase() {
    race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];
    for (int i = 0; i < THREADS_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increase();
                }
            }
        });
        threads[i].start();
    }
    while (Thread.activeCount() > 2) {
        Thread.yield();
    }
    System.out.println(race);
}
}

书本上 Thread.activeCount() > 1 是有问题的,我这篇博客有解释

运行结果:不是200000

volatile能保证有序性

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

举个例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。


volatile使用场景

在某些情况下,volatile 的同步机制的性能确实要优于锁。

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  2. 变量不需要与其他状态变量共同参与不变约束

使用场景1:

volatile boolean shutdownRequested;
public void shutdown(){
    shutdownRequested = true;
}
public void doWork(){
    while(!shutdownRequested){
        //do stuff
    }
}

使用场景2:指令重排序

Map configOptions;
char[] configText;
volatile boolean initialized = false;
// 线程1
//模拟读取配置信息,当读完后将 initialized 设置为 true 以后通知其它线程配置可用
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;
// 线程2
while(!initialized){
   sleep();
}
//使用线程 1 中初始化好的配置信息
doSomethingWithConfig();

使用场景3:可以使用 volatile 关键字来保证多线程下的单例

public class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
    if(instance==null) {
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();
        }
    }
    return instance;
}
}

总结

参考

深入理解JVM

https://www.cnblogs.com/dolphin0520/p/3920373.html

主要讲了Java内存模型,已经并发的原子性、可见性、有序性,以及 volatile关键字。

有什么错误欢迎指出,十分感谢!

原文地址:https://www.cnblogs.com/rookieJW/p/9362387.html

时间: 2024-10-16 15:40:44

Java 内存模型与线程的相关文章

java内存模型与线程(转) good

java内存模型与线程 参考 http://baike.baidu.com/view/8657411.htm http://developer.51cto.com/art/201309/410971_all.htm http://www.cnblogs.com/skywang12345/p/3447546.html 计算机的CPU计算能力超强,其计算速度与 内存等存储 和通讯子系统的速度相比快了几个数量级, 数据加载到内存中后,cpu处理器运算处理时,大部分时间花在等待获取去获取磁盘IO.网络通

Java并发程序设计(三) Java内存模型和线程安全

Java内存模型和线程安全 一 .原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰. 思考:i++是原子操作吗?  二.有序性 Java代码在执行使,并不一点会按照编写程序的语义顺序执行(为了优化性能).具体不做解释. 三.可见性 可见性是指一个线程修改了一个共享变量的值,其他线程能否立即知道这个修改. 编译器优化硬件优化(如写吸收,批操作) Java虚拟机层面的可见性  public class VisibilityTest ext

[笔记]JAVA内存模型与线程

JAVA线程  工作内存  主内存 java内存模型中的八种操作: lock    unlock    read     load     use      assign      store     write 八种基本操作必须满足的规则 volatile 当一个变量被定义成volatile之后,它将具备两种特性 一是保证此变量对所有线程的可见性("可见性"是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的.) 二是禁止指令重排序优化(普通的变量仅仅会保证在该方法

jvm(12)-java内存模型与线程

[0]README 0.1)本文部分文字描述转自“深入理解jvm”,旨在学习“java内存模型与线程” 的基础知识: [1]概述 1)并发处理的广泛应用是使得 Amdahl 定律代替摩尔定律称为计算机性能发展源动力的根本原因: 2)Amdahl 定律:该定律通过系统中并行化与串行化的比重来描述多处理器系统能获得的运算加速能力: 3)摩尔定律:该定律用于描述处理器晶体管数量与运行效率间的发展关系: Conclusion)这两个定律的更替代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展

深入理解java虚拟机-第12章Java内存模型与线程

第12章 Java内存模型与线程 Java内存模型  主内存与工作内存: java内存模型规定了所有的变量都在主内存中,每条线程还有自己的工作内存. 工作内存中保存了该线程使用的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行. 内存间交互操作: 1 lock 作用于主内存的变量,它把一个变量标识为一个线程独占的状态. 2 unlock 作用于主内存的变量,把锁定的变量释放出来 3 read 作用于工作内存的变量,把一个变量的值从主内存传输到线程的工作内存中. 4 load 作用于工作

(Java多线程系列七)Java内存模型和线程的三大特性

Java内存模型和线程的三大特性 多线程有三大特性:原子性.可见性.有序性 1.Java内存模型 Java内存模型(Java Memory Model ,JMM),决定一个线程对共享变量的写入时,能对另一个线程可见.从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本.本地内存是JMM的一个抽象概念,并不真实存在. 用一张图表示

Java内存模型与线程

写在前面:与之前主流程序语言(c/c++等)直接使用物理硬件和操作系统的内存模型不同,java虚拟机为了屏蔽各种硬件和操作系统的内存访问差异定义了一种java内存模型.其主要定义程序中各个变量的访问规则(在虚拟机中将变量存储到内存和从内存中取出变量的底层细节). 线程.主内存.工作内存之间的交互关系 1.java内存模型结构: -所有的变量都存储在主内存中. -每条线程还有自己的工作内存. -工作内存中保存了从主内存中拷贝的该线程所要使用到的变量 -每条线程对变量的操作必须在自己的工作内存中,不

深入理解Java虚拟机- 学习笔记 - Java内存模型与线程

除了在硬件上增加告诉缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证.与处理器的乱序优化执行类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction

011 Java内存模型与线程

1.Java内存模型 Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果. Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节. ①主内存与工作内存 Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类

第12章 Java内存模型与线程

1. Java内存模型 Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的并发效果.在此之前,主流程序语言直接使用物理硬件(或者说是操作系统的内存模型),因此会由于不同平台上内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问却经常出错. 1. 主内存与工作内存 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.此处的