zoukankan      html  css  js  c++  java
  • Java并发编程实战(2)- Java内存模型

    本文主要描述了在Java并发编程中非常重要的Java内存模型以及Happens-Before规则。

    概述

    对于Java并发程序问题存在的各种问题,主要有3个根源:

    • 由缓存引发的可见性问题
    • 由线程切换引发的原子性问题
    • 由编译优化引发的有序性问题

    为了解决可见性和有序性的问题,Java引入了Java内存模型,我们这篇文章来介绍一下它。

    可见性问题和有序性问题由缓存和编译优化造成的, 那么最直接的方法就是禁用缓存和编译优化,这样做是可以解决问题的,但是程序的性能会下降到不能接受的程度。

    合理的方案是按需禁用缓存和编译优化, 所谓“按需禁用”,就是指按照程序员的要求来禁用,来为程序员开放相应的方法。

    什么是Java内存模型

    Java内存模型是一个很复杂的规范,可以从不同的角度进行解读,站在程序员的角度,可以将其解决为它规范了JVM如何提供按需禁用缓存和编译优化的方法。

    Java内存模型对应的规范是JSR-133,链接:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf.

    Java内存模型和JVM的区别

    • Java内存模型定义了一套规范,它能让JVM按需禁用CPU缓存和编译优化,这套规范包括volatile、synchronized、final三个关键字和7个Happen-Before规则。
    • JVM内存模型是指程序计数器、JVM方法栈、本地方法栈、堆、方法区这5部分。

    volatile关键字

    volatile关键字的用途是禁用CPU缓存。

    例如我们定义一个volatile变量volatile int x = 0;,它表达的是:编译器在对这个变量进行读写操作时,不能使用CPU缓存,而是从内存中直接操作。

    我们来看下面的代码示例。

    public class VolatileDemo {
    
    	int x = 0;
    	volatile boolean v = false;
    	
    	public void write() {
    		x = 42;
    		v = true;
    	}
    	
    	public void read() {
    		if (v == true) {
    			System.out.println(String.format("x is %s", x));
    		}
    	}
    }
    

    如果对同一个VolatileDemo对象,有2个线程,一个调用write()方法,一个调用read()方法,那么当read()方法中v等于true时,x的值是多少?

    在Java 1.5版本之前,x的值可能是0或者42, 在Java 1.5版本之后,x的值只能是42。

    这是由于Happens-Before规则导致的。

    Happens-Before规则

    什么是Happens-Before规则?

    Happens-Before规则表达的是前面一个操作的结果对后续操作是可见的。它约束了编译器的优化行为,保证其一定要遵守Happens-Before规则。

    Happens-Before的语义本质是一种可见性,A Happens-Before B意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程中。

    Happens-Before规则有很多条,其中和程序员相关的有6条,我们来一一描述。

    顺序性规则

    在一个线程中,按照程序顺序,前面的操作Happens-Before后续的任意操作。

    这条规则比较直观,符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。

    volatile变量规则

    对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。

    传递性

    如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

    我们再看上面的示例代码:

    • x=42 Happens-Before 写变量 v=true,这是规则1。
    • 写变量 v=true Happens-Before 读变量 v=true,这是规则2。

    然后根据传递性规则,我们可以得出x=42 Happens-Before 读变量 v=true。所以在示例代码中,在判断v==true时,x的值等于42。

    synchronized规则

    对一个锁的解锁要Happens-Before于后续对这个锁的加锁。

    我们要首先了解什么是“管程”,管程是操作系统中的一个重要概念,一个管程是一个由过程、变量及数据结构等组成的一个集合,它由四个部分组成:1)管程名称,2)共享数据的说明,3)对数据进行操作的一组过程,4)对共享数据赋初值的语句。

    在Java中,管程是通过synchronized关键字实现的。

    我们对这个规则可以理解为:假设x的初始值是10,线程A获取锁,执行完代码,x的值会变为12,之后释放锁,接下来线程B获取锁,这时线程B看到的x,一定是12,不应该是10。

    线程start()规则

    主线程A启动子线程B,子线程B能够看到主线程在启动子线程B之前的操作。

    我们来看下面的示例。

    public class HappensBeforeDemo {
    
    	private int x = 10;
    	
    	public void threadStartTest() {
    		Thread t = new Thread(() -> {
    			System.out.println(String.format("x is %s.",x));
    		}
    		);
    		
    		x = 20;
    		
    		t.start();
    	}
    	
    	
    	public static void main(String[] args) {
    		HappensBeforeDemo demoObj = new HappensBeforeDemo();
    		demoObj.threadStartTest();
    		
    	}
    }
    

    程序的输出结果如下。

    x is 20.
    

    线程join()规则

    主线程A通过调用子线程的join()方法等待子线程结束,当子线程结束后,主线程能够看到子线程对共享变量的操作。

    这个规则和线程start()规则类似,我们来看下面的示例代码。

    public class HappensBeforeDemo {
    
    	private int x = 10;
    	
    	
    	public void threadJoinTest() throws InterruptedException {
    		Thread t = new Thread(() -> {
    			try {
    				java.lang.Thread.sleep(3000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			x = 30;
    		}) ;
    		
    		t.start();
    		t.join();
    		System.out.println(String.format("x is %s.",x));
    		
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		HappensBeforeDemo demoObj = new HappensBeforeDemo();
    		demoObj.threadJoinTest();
    		
    	}
    }
    

    程序的输出结果如下。

    x is 30.
    

    final规则

    我们用final修饰变量时,就是告诉编译器,这个变量生而不变,可以尽情优化。

    但是如果我们将变量设置成final,它的构造函数由于编译优化后的错误重排,还是可能会导致错误,例如我们之前谈到的单例模式的代码。

    在Java 1.5之后,Java内存模型对final类型变量的重排进行了约束,只要我们提供的构造函数没有“逸出”,那么就不会有问题。

    所谓“逸出”,就是指构造函数中使用了生命周期超过了该对象生命周期的变量。

    参考资料

  • 相关阅读:
    BestCoder17 1001.Chessboard(hdu 5100) 解题报告
    codeforces 485A.Factory 解题报告
    codeforces 485B Valuable Resources 解题报告
    BestCoder16 1002.Revenge of LIS II(hdu 5087) 解题报告
    codeforces 374A Inna and Pink Pony 解题报告
    codeforces 483B Friends and Presents 解题报告
    BestCoder15 1002.Instruction(hdu 5083) 解题报告
    codeforces 483C.Diverse Permutation 解题报告
    codeforces 483A. Counterexample 解题报告
    NSArray中地内存管理 理解
  • 原文地址:https://www.cnblogs.com/wing011203/p/14240786.html
Copyright © 2011-2022 走看看