[高并发Java 三] Java内存模型和线程安全

网上不少资料在描述Java内存模型的时候,都会介绍有一个主存,而后每一个工做线程有本身的工做内存。数据在主存中会有一份,在工做内存中也有一份。工做内存和主存之间会有各类原子操做去进行同步。 java

下图来源于这篇Blog git

可是因为Java版本的不断演变,内存模型也进行了改变。本文只讲述Java内存模型的一些特性,不管是新的内存模型仍是旧的内存模型,在明白了这些特性之后,看起来也会更加清晰。 github

1. 原子性

  • 原子性是指一个操做是不可中断的。即便是在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其它线程干扰。

通常认为cpu的指令都是原子操做,可是咱们写的代码就不必定是原子操做了。 设计模式

好比说i++。这个操做不是原子操做,基本分为3个操做,读取i,进行+1,赋值给i。 缓存

假设有两个线程,当第一个线程读取i=1时,还没进行+1操做,切换到第二个线程,此时第二个线程也读取的是i=1。随后两个线程进行后续+1操做,再赋值回去之后,i不是3,而是2。显然数据出现了不一致性。 安全

再好比在32位的JVM上面去读取64位的long型数值,也不是一个原子操做。固然32位JVM读取32位整数是一个原子操做。 多线程

2. 有序性

  • 在并发时,程序的执行可能就会出现乱序。

计算机在执行代码时,不必定会按照程序的顺序来执行。 并发

class OrderExample { 
		int a = 0; 
		boolean flag = false; 
		public void writer() 
		{ 
			a = 1; 
			flag = true; 
		} 
		public void reader() 
		{ 
			if (flag) 
			{ 
				int i = a +1;  
			}
		} 
	}
好比上述代码,两个方法分别被两个线程调用。按照常理,写线程应该先执行a=1,再执行flag=true。当读线程进行读的时候,i=2;

可是由于a=1和flag=true,并无逻辑上的关联。因此有可能执行的顺序颠倒,有可能先执行flag=true,再执行a=1。这时当flag=true时,切换到读线程,此时a=1尚未执行,那么读线程将i=1。 app

固然这个不是绝对的。是有可能会发生乱序,有可能不发生。 jvm

那么为何会发生乱序呢?这个要从cpu指令提及,Java中的代码被编译之后,最后也是转换成汇编码的。

一条指令的执行是能够分为不少步骤的,假设cpu指令分为如下几步

  • 取指 IF
  • 译码和取寄存器操做数 ID
  • 执行或者有效地址计算 EX
  • 存储器访问 MEM
  • 写回 WB
假设这里有两条指令

通常来讲咱们会认为指令是串行执行的,先执行指令1,而后再执行指令2。假设每一个步骤须要消耗1个cpu时间周期,那么执行这两个指令须要消耗10个cpu时间周期,这样作效率过低。事实上指令都是并行执行的,固然在第一条指令在执行IF的时候,第二条指令是不能进行IF的,由于指令寄存器等不能被同时占用。因此就如上图所示,两条指令是一种相对错开的方式并行执行。当指令1执行ID的时候,指令2执行IF。这样只用6个cpu时间周期就执行了两个指令,效率比较高。

按照这个思路咱们来看下A=B+C的指令是如何执行的。

如图所示,ADD操做时有一个空闲(X)操做,由于当想让B和C相加的时候,在图中ADD的X操做时,C还没从内存中读取(当MEM操做完成时,C才从内存中读取。这里会有一个疑问,此时尚未回写(WB)到R2中,怎么会将R1与R1相加。那是由于在硬件电路当中,会使用一种叫“旁路”的技术直接把数据从硬件当中读取出来,因此不须要等待WB执行完才进行ADD)。因此ADD操做中会有一个空闲(X)时间。在SW操做中,由于EX指令不能和ADD的EX指令同时进行,因此也会有一个空闲(X)时间。

接下来举个稍微复杂点的例子

a=b+c 
d=e-f

对应的指令以下图

缘由和上面的相似,这里就不分析了。咱们发现,这里的X不少,浪费的时间周期不少,性能也被影响。有没有办法使X的数量减小呢?

咱们但愿用一些操做把X的空闲时间填充掉,由于ADD与上面的指令有数据依赖,咱们但愿用一些没有数据依赖的指令去填充掉这些由于数据依赖而产生的空闲时间。

咱们将指令的顺序进行了改变

改变了指令顺序之后,X被消除了。整体的运行时间周期也减小了。

指令重排可使流水线更加顺畅

固然指令重排的原则是不能破坏串行程序的语义,例如a=1,b=a+1,这种指令就不会重排了,由于重排的串行结果和原先的不一样。

指令重排只是编译器或者CPU的优化一种方式,而这种优化就形成了本章一开始程序的问题。

如何解决呢?用volatile关键字,这个后面的系列会介绍到。

3. 可见性

  • 可见性是指当一个线程修改了某一个共享变量的值,其余线程是否可以当即知道这个修改。

可见性问题可能有各个环节产生。好比刚刚说的指令重排也会产生可见性问题,另外在编译器的优化或者某些硬件的优化都会产生可见性问题。

好比某个线程将一个共享值优化到了内存中,而另外一个线程将这个共享值优化到了缓存中,当修改内存中值的时候,缓存中的值是不知道这个修改的。

好比有些硬件优化,程序在对同一个地址进行屡次写时,它会认为是没有必要的,只保留最后一次写,那么以前写的数据在其余线程中就不可见了。

总之,可见性的问题大多都源于优化。

接下来看一个Java虚拟机层面产生的可见性问题

问题来自于一个Blog

package edu.hushi.jvm;
 
/**
 *
 * @author -10
 *
 */
public class VisibilityTest extends Thread {
 
    private boolean stop;
 
    public void run() {
        int i = 0;
        while(!stop) {
            i++;
        }
        System.out.println("finish loop,i=" + i);
    }
 
    public void stopIt() {
        stop = true;
    }
 
    public boolean getStop(){
        return stop;
    }
    public static void main(String[] args) throws Exception {
        VisibilityTest v = new VisibilityTest();
        v.start();
 
        Thread.sleep(1000);
        v.stopIt();
        Thread.sleep(2000);
        System.out.println("finish main");
        System.out.println(v.getStop());
    }
 
}
代码很简单,v线程一直不断的在while循环中i++,直到主线程调用stop方法,改变了v线程中的stop变量的值使循环中止。

看似简单的代码运行时就会出现问题。这个程序在 client 模式下是能中止线程作自增操做的,可是在 server 模式先将是无限循环。(server模式下JVM优化更多)

64位的系统上面大多都是server模式,在server模式下运行:

finish main
true
只会打印出这两句话,而不会打印出finish loop。但是可以发现stop的值已是true了。

该Blog做者用工具将程序还原为汇编代码

这里只截取了一部分汇编代码,红色部分为循环部分,能够清楚得看到只有在0x0193bf9d才进行了stop的验证,而红色部分并无取stop的值,因此才进行了无限循环。

这是JVM优化后的结果。如何避免呢?和指令重排同样,用volatile关键字。

若是加入了volatile,再还原为汇编代码就会发现,每次循环都会get一下stop的值。

接下来看一些在“Java语言规范”中的示例


上图说明了指令重排将会致使结果不一样。


上图使r5=r2的缘由是,r2=r1.x,r5=r1.x,在编译时直接将其优化成r5=r2。最后致使结果不一样。

4. Happen-Before

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每个动做
  • 线程的全部操做先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法
这些原则保证了重排的语义是一致的。

5. 线程安全的概念

指某个函数、函数库在多线程环境中被调用时,可以正确地处理各个线程的局部变量,使程序功能正确完成。

好比最开始所说的i++的例子

就会致使线程不安全。

关于线程安全的详情使用,请参考之前写的这篇Blog,或者关注后续系列,也会谈到相关内容。






系列:

[高并发Java 一] 前言

[高并发Java 二] 多线程基础

[高并发Java 三] Java内存模型和线程安全

[高并发Java 四] 无锁

[高并发Java 五] JDK并发包1

[高并发Java 六] JDK并发包2

[高并发Java 七] 并发设计模式

[高并发Java 八] NIO和AIO

[高并发Java 九] 锁的优化和注意事项

[高并发Java 十] JDK8对并发的新支持

相关文章
相关标签/搜索