java多线程12.内存模型

假设一个线程为变量赋值:variable = 3;

内存模型需要解决一个问题:“在什么条件下,读取variable的线程将看到这个值为3?”

这看上去理所当然,但是如果缺少内存同步,那么将会有许多因素使得线程无法立即甚至永远,看到另一个线程的操作结果。

如:

  • 1.在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会将变量保存在寄存器而不是内存中;
  • 2.处理器可以采用乱序或并行等方式来执行指令;
  • 3.缓存可能会改变将写入变量提交到主内存的次序;
  • 4.而且保存在处理器本地缓存中的值,对于其他处理器是不可见的。

这些因素都会使得一个线程无法看到变量的最新值,并且会导致其他线程中的内存操作似乎在乱序执行。

Java语言规范要求JVM在线程中维护一种类似串行的语义:只要程序的最终结果与严格串行环境中执行的结果相同,那么上述所有的操作都是允许的。

这确实是一件好事,因为计算机近年来在性能上的提升很大程度要归功于这些重新排序措施。

在单线程环境中,我们无法看到所有这些底层技术,它们除了提高程序的执行速度外,不会产生其他影响。

在多线程环境中,要维护程序的串行性将导致很大的性能开销。对于并发应用程序中的线程来说,它们在大部分时间里都执行各自的任务,因此在线程之间的协调操作只会降低应用程序的运行速度,而不会带来任何好处。只有当多个线程要共享数据时,才必须协调它们之间的操作,并且JVM依赖程序通过同步操作来找出这些协调操作将在何时发生。

JVM规定了一组最小保证,这组保证规定了对变量的写入操作将在何时对于其他线程可见。JVM在设计时就在可预测性和程序的易于开发性之间进行了权衡,从而在各种主流的处理器体系架构上都能实现高性能的JVM。

平台的内存模型

在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期地与主内存进行协调。

在不同的处理器架构中提供了不同级别的缓存一致性,其中一部分只提供最小的保证,即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。

要想确保每个处理器都能在任意时刻知道其他处理器正在进行的工作,将需要非常大的开销。在大多数时间里,这种信息是不必要的,因此处理器会适当放宽存储一致性保证,以换取性能的提升。

在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证。为了使Java开发人员无须关心不同架构上内存模型之间的差异,Java还提供了自己的内存模型,并且JVM通过在适当的位置插上内存栅栏来屏蔽在JVM与底层平台内存模型之间的差异。

假设:想象在程序中只存在唯一的操作执行顺序,而不考虑这些操作在何种处理器上执行,并且在每次读取变量时,都能获得在执行序列中(任何处理器)最近一次写入该变量的值。

这种乐观的模型被称为串行一致性,开发人员经常会错误的假设存在串行一致性,但在任何一款现代多处理器架构中都不会提供这种串行一致性,JVM也如此。

在支持共享内存的多处理器和编译器中,当跨线程共享数据时,会出现一些奇怪的情况,除非通过使用内存栅栏来防止这些情况的发生。不过在Java程序中不需要指定内存栅栏的位置,而只需要通过正确地使用同步来找出何时将访问共享状态。

重排序

/**
 * 在没有正确同步的情况下,即使要推断最简单的并发程序的行为也很困难。
 * 示例中:很容易想象如何输出(1,0),(0,1)或(1,1),T1可以在T2开始之前完成,T2也可以在T1开始之前完成,或者二者交替执行。
 * 但还可以输出(0,0),由于每个线程中的各个操作之间不存在数据流依赖性,因此这些操作可以乱序执行(即使这些操作按照顺序执行,但在将
 * 缓存刷新到主内存的不同时序中也可能出现这种情况,在T2的角度看,T1的赋值操作可能以相反的次序执行)。
 * 可以想象在T2看来的执行顺序[ x=b, b=1, y=a, a=1 ]
 * 要列举出这个简单示例的所有可能执行结果非常困难,内存级的重排序会使程序的行为变得不可预测。
 * 而要确保在程序中正确地使用同步却非常容易,同步将限制 编译器、运行时和硬件对内存操作重排序的方式,从而在重排序时不会破坏JVM提供的可见性保证。
 */
public class Demo{

    static int x = 0, y = 0;
    static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread T1  = new Thread(new Runnable(){
            public void run() {
                a = 1;
                x = b;
            }
        });

        Thread T2 = new Thread(new Runnable(){
            public void run() {
                b = 1;
                y = a;
            }
        });

        T1.start();
        T2.start();

        T1.join();
        T2.join();

        System.out.println(x +"--"+y);
    }
}

Java内存模型

java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。

JVM为程序中所有的操作定义了一个偏序关系,称为Happens-Before。要想保证执行操作B的线程看到操作A的结果(无论A和B是否在同一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果缺乏这个关系,那么JVM可以对它们任意的重排序。

当一个变量被多个线程读取并且至少被一个线程写入时,如果在读操作和写操作之间没有依照Happens-Before来排序,那么就会产生数据竞争问题。在正确同步的程序中不存在数据竞争,并会表现出串行一致性,就是说程序中的所有操作都会按照一种固定的和全局的顺序执行。

  • Happens-Before规则:
  • 1.程序顺序规则:如果程序中操作A在操作B之前,那么线程中操作A将在操作B之前执行。
  • 2.监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • 3.volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 4.线程启动规则:在线程上对Thread.Start的调用必须在该线程中执行任何操作之前执行。
  • 5.线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  • 6.终端规则:当一个线程在另一个线程上调用interrupt时,必须在被终端线程检测到interrupt调用之前执行通过抛出(InterruptException,或者调用isInterrupted和interrupted。)
  • 7.终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 8.传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

虽然这些操作只满足偏序关系,但同步操作,如锁的获取与释放,以及volatile变量的读取与写入操作,都满足全序关系。

因此,在描述Happens-Before关系时,就可以使用“后续的锁获取操作”和“后续的volatile变量读取操作”等表达术语。

当两个线程使用同一个锁进行同步时,在它们之间存在Happens-Before关系。

在线程A内部的所有操作都按照它们在源程序中的先后顺序来排序,在线程B内部的操作也是如此。在A释放了锁M,并且B随后获取了锁M,因此A中所有在释放锁之前的操作,就位于B中请求锁之后的所有操作之前。而如果两个线程是在不同的锁上进行同步的,那么就不能推断它们之间的动作顺序,因为两个线程之间并不存在Happens-Before关系。

借助同步

将Happens-Before的程序顺序规则与其他某个顺序规则(通常是监视器锁规则或volatile变量规则)结合起来,从而对某个未被锁保护的变量的访问操作进行排序。

在FutureTask的保护方法AbstractQueuedSynchronizer中说明了如何使用这种“借助”技巧。

AQS维护了一个表示同步器状态的整数,FutureTask用这个整数来保存任务的状态。但FutureTask还维护了其他一些变量,如计算结果。

当一个线程调用set来保存结果并且另一个线程调用get来获取结果时,这两个线程最好按照Happens-Before进行排序。这可以将执行结果的引用声明为volatile类型来实现,但利用现有的同步机制可以更容易地实现相同的功能。

/**
 * FutureTask在设计时能够确保,在调用tryAcquireShared之前总能成功地调用tryReleaseShared。
 * tryReleaseShared会写入一个volatile类型的变量,而tryAcquireShared将读取这个变量。
 * 在保存和获取result时将调用innerSet和innerGet方法。
 * 由于innerSet将在调用releaseShared(这又将调用tryReleaseShared)之前写入result,
 * 并且innerGet将在调用acquireShared(这又将调用tryAcquireShared)之后读取result,
 * 因此就可以确保innerSet的写入操作在innerGet中的读取操作之前执行。
 *
 * @param <V>
 */
public class FutureTask<V>{

    private final class Sync extends AbstractQueuedSynchronizer{

        private static final int RUNNING = 1;

        private static final int RAN = 2;

        private static final int CANCELLED = 4;

        private V result;

        private Exception exception;

        void innerset(V v){
            while(true){
                int s = getState();
                if(ranOrCancelled(s)){
                    return;
                }
                if(compareAndSetState(s,RAN)){
                    break;
                }
            }
            result = v;
            releaseShared(0);
            done();
        }

        V innerGet() throws InterruptedException, ExecutionException{
            acquireSharedInterruptibly(0);
            if(getState() == CANCELLED){
                throw new CancellationException();
            }
            if(exception != null){
                throw new ExecutionException(exception);
            }
            return result;
        }
    }
}
  • 类库中提供的其他Happens-Before排序如:
  • 1.将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行。
  • 2.在CountDownLatch上的倒数操作将在线程从闭锁上的await方法中返回之前执行。
  • 3.在释放Semaphore许可操作将在从该Semaphore上获得一个许可之前执行。
  • 4.Future表示的任务的所有操作将在从Future.get中返回之前执行。
  • 5.向Exceutor提交一个Runnable或Callable的操作将在任务开始之前执行。
  • 6.一个线程到达CyclicBarrier或Exchanger的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果CyclicBarrier使用一个栅栏操作,那么到达栅栏的操作将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。

不安全的发布

当缺少Happens-Before关系时,就可能出现重排序问题,这可以解释为什么在没有充分同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。

在初始化一个新的对象时需要写入多个变量,即新对象中的各个域。同样,在发布一个引用时也需要写入一个变量,即新对象的引用。

如果无法确保发布共享引用的操作在另一个线程加载该共享引用之前执行,那么对新对象引用的写入操作与对象中各个域的写入操作重排序(从使用该对象的线程的角度来看)。

/**
 * 程序中存在的问题似乎只有竞态条件问题(当所有Resource示例都相同时可以忽略)
 * 即使不考虑这个问题,这样发布仍然是不安全的,因为在另一个线程可能看到部分构造的Resource实例的引用。
 *
 * 假设T1是第一个调用getInstance的线程,它将看到resource为null,并且初始化一个新的Resource,然后将resource设置为这个新实例。
 * 当T2随后调用getInstance,它可能看到resource值为非空,因此使用这个已经构造好的Resource。
 * 但T1写入resource的操作与T2读取resource的操作之间并不存在Happens-Before方法。
 *
 * 当新分配一个Resource时,Resource的构造函数将把新实例中的各个域由默认值修改为初始值。
 * 由于两线程未使用同步,因此T2看到的T1的操作顺序,可能与T1执行这些操作时的顺序不同。
 * 即T2可能看到对resource的写入操作将在Resource各个域的写入操作之前发生。 从而T2就看到一个被部分构造处于无效状态的Resource实例。
 */
public class Resource{

    private static Resource resource;

    public static Resource getInstance(){
        if(resource == null){
            resource = new Resource();
        }
        return resource;
    }
}

除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。

安全发布

在上面示例中需要将getInstance改为synchronized,使用同步即可解决问题。

JVM在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。

静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且线程使用之前。

由于JVM将在初始化期间获得一个锁,并且每个线程只是获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存的写入操作将自动对所有线程可见。

因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显示的同步。

然而这个规则仅适用于在构造时的状态,如果对象时可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。

将静态初始化器这种特性和JVM的延迟加载机制结合起来,可以形成一种延迟初始化技术。

/**
 * 延迟初始化占位模式:使用一个专门的类来初始化Resource。
 * JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。
 * 当任何一个线程第一次调用getResource时,都会使ResourceHolder被加载和初始化,此时静态初始化器将执行
 */
public class ResourceFactory{

    private static class ResourceHolder{
        public static Resource resource = new Resource();
    }

    public static Resource getResource(){
        return ResourceHolder.resource;
    }
}

#笔记内容参考 《java并发编程实战》

原文地址:https://www.cnblogs.com/shanhm1991/p/9900731.html

时间: 2024-11-05 16:30:52

java多线程12.内存模型的相关文章

Java虚拟机:内存模型详解

版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 我们都知道,当虚拟机执行Java代码的时候,首先要把字节码文件加载到内存,那么这些类的信息都存放在内存中的哪个区域呢?当我们创建一个对象实例的时候,虚拟机要为对象分配内存,Java虚拟机又是如何配分内存的呢?这些都涉及到Java虚拟机的内存划分机制,今天我们就来探究一下Java虚拟机的内存模型. Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间,有的区域随

进程与线程(二) java进程的内存模型

从我出生那天起,我就知道我有个兄弟,他桀骜不驯,但实力强悍 ,人家都叫它C+++            ----java 上回说到了,C进程的内存分配,那么一个java运行过程也是一个进程,java内存是如何分配的呢? http://blog.csdn.net/shimiso/article/details/8595564 详情请看:http://blog.csdn.net/a859522265/article/details/7282817 1.学习java,学过java多线程,没有学过多进程

关于JAVA中的static方法、并发问题以及JAVA运行时内存模型

一.前言 最近在工作上用到了一个静态方法,跟同事交流的时候,被一个问题给问倒了,只怪基础不扎实... 问题大致是这样的,"在多线程环境下,静态方法中的局部变量会不会被其它线程给污染掉?": 我当时的想法:方法中的局部变量在运行的时候,是存在JAVA栈中的,方法运行结束,局部变量也就都弹光了,理论上单线程的话是不会有问题的,我之所以不知道,是因为不清楚在JAVA内存模型中,一个线程对应一个栈,还是多个线程共享一个栈... 其实如果知道每个线程都有一个自己的JAVA栈的话,问题也就很清楚了

java多线程03-----------------volatile内存语义

java多线程02-----------------volatile内存语义 volatile关键字是java虚拟机提供的最轻量级额的同步机制.由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过). 1. java内存模型(JMM) JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存的数据不一致.编译器处理器的指令重排序造成的各种线程安全问题,以保障多线程编程的原

java虚拟机之内存模型

1. 概述 对于从事 C.C++ 程序开发的人员来说,在内存管理领域,他们既是拥有最高权力的「皇帝」又是从事基础工作的「劳动人民」 --- 既拥有每个对象的「所有权」,又担负着每一个对象生命开始到终结的维护责任. 但是对于 java 程序员来说,在虚拟机自动内存管理机制的帮助下,不需要再为每一个 new 操作写配对的 delete/free 代码,不容易出现在内存泄漏和内存溢出问题,由虚拟机管理内存这一切看起来都很美好.不过,也正是因为 java 程序员把内存控制的权利交给了 java 虚拟机,

java多线程 生产消费者模型

[seriesposts sid=500] 下面的代码讲述了一个故事 一个面包生产铺里目前有30个面包,有三个人来买面包,第一个人要买50个,第二个要买20个,第三个要买30个. 第一个人不够,所以等着,让第二个买了.面包铺继续生产面包.有7个人在生产. package com.javaer.thread; public class CPMode { public static void main(String[] args) { Godown godown = new Godown(30);

java中高级面试题, 虚拟机,JVM调优,垃圾回收,多线程,内存模型

面试问题: 一.Java基础方面: 1.Java面相对象的思想的理解(主要是多态): http://blog.csdn.net/zhaojw_420/article/details/70477636 2.集合:ArrayList,LinkedList,HashMap,LinkedHashMap,ConcurrentHashMap,HashTable,HashSet的底层源码实现原理 3.Java虚拟机 (1)组成以及各部分作用: http://blog.csdn.net/zhaojw_420/a

java多线程之内存可见性-synchronized、volatile

1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁需要是同一把锁) 注:线程解锁前对共享变量的修改在下次加锁时对其他线程可见 2.线程执行互斥代码的过程: 1.获得互斥锁 2.清空工作内存 3.从主内存拷贝变量的最新副本到工作内存 4.执行代码 5.将更改后的共享变量的值刷

java多线程与内存可见性

一.java多线程 JAVA多线程实现的三种方式: http://blog.csdn.net/aboy123/article/details/38307539 二.内存可见性 1.什么是JAVA 内存模型 共享变量 :如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存