java并发编程12.java内存模型

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

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

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

如:

1.在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会将变量保存在寄存器而不是内存中;

2.处理器可以采用乱序或并行等方式来执行指令;

3.缓存可能会改变将写入变量提交到主内存的次序;

4.而且保存在处理器本地缓存中的值,对于其他处理器是不可见的。

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

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

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

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

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

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

平台的内存模型

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

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

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

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

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

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

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

重排序

/**
 * @author 83921
 * 在没有正确同步的情况下,即使要推断最简单的并发程序的行为也很困难。在示例中:
 * 很容易想象如何输出(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类型来实现,但利用现有的同步机制可以更容易地实现相同的功能。

import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * @author 83921
 * FutureTask在设计时能够确保,在调用tryAcquireShared之前总能成功地调用tryReleaseShared。
 * tryReleaseShared会写入一个volatile类型的变量,而tryAcquireShared将读取这个变量。
 * 在保存和获取result时将调用innerSet和innerGet方法。
 * 由于innerSet将在调用releaseShared(这又将调用tryReleaseShared)之前写入result,
 * 并且innerGet将在调用acquireShared(这又将调用tryAcquireShared)之后读取result,
 * 因此就可以确保innerSet的写入操作在innerGet中的读取操作之前执行。
 */
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关系时,就可能出现重排序问题,这可以解释为什么在没有充分同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。

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

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

在这种情况下,另一个线程可能看到对象引用的最新值。但同时也可能看到对象的某些或全部状态中包含的是无效值,即一个被部分构造的对象。

/**
 * @author 83921
 * 程序中存在的问题似乎只有竞态条件问题(当所有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的延迟加载机制结合起来,可以形成一种延迟初始化技术。

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

#笔记内容来自 《java并发编程实战》

时间: 2024-10-10 12:34:10

java并发编程12.java内存模型的相关文章

4.java并发编程艺术-java并发编程基础

java从诞生开始就明智的选择了内置对多线程的支持,这使得java语言相比同一时期的其他语言具有明显的优势.线程作为操作系统调度的最小单元,多个线程能够同时执行,这将显著提升程序的性能,在多核环境中表现的更加明显.但是,过多的创建线程和对线程的不当管理也容易造成问题.本章将着重介绍java并发编程的基础知识,从启动一个线程到线程间不同的通信方式,最后通过简单的线程池示例以及应用(简单的Web服务器)来串联本章所介绍的内容. 1.线程简介 1.1 什么是线程 现代操作系统中在运行一个程序时,会为其

3.java并发编程艺术-java内存模型

3.1 java内存模型的基础 3.1.1并发编程模型的两个关键问题 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指两个线程 之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信.在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来进行显式进行通信. 同步是指程序中用于控制不同线程

Java并发编程--7.Java内存操作总结

主内存和工作内存 工作规则 Java内存模型, 定义变量的访问规则, 即将共享变量存储到内存和取出内存的底层细节  所有的变量都存储在主内存中,每条线程有自己的工作内存,工作内存中用到的变量, 是从主内存拷贝的副本,线程对变量的所有操作都在工作内存中进行, 线程间变量值得传递均需通过主内存来完成 内存间交互操作 1.luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态. 2.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其

Java并发编程:Java线程池核心ThreadPoolExecutor的使用和原理分析

目录 引出线程池 Executor框架 ThreadPoolExecutor详解 构造函数 重要的变量 线程池执行流程 任务队列workQueue 任务拒绝策略 线程池的关闭 ThreadPoolExecutor创建线程池实例 参考: 引出线程池 线程是并发编程的基础,前面的文章里,我们的实例基本都是基于线程开发作为实例,并且都是使用的时候就创建一个线程.这种方式比较简单,但是存在一个问题,那就是线程的数量问题. 假设有一个系统比较复杂,需要的线程数很多,如果都是采用这种方式来创建线程的话,那么

Java并发编程:Java中的锁和线程同步机制

锁的基础知识 锁的类型 锁从宏观上分类,只分为两种:悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作.Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败. 悲观

Java并发编程:Java ConcurrentModificationException异常原因和解决方法

Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常.下面我们就来讨论以下这个异常出现的原因以及解决办法. 以下是本文目录大纲: 一.ConcurrentModificationException异常出现的原因 二.在单线程环境下的解决办法 三.在多线程环境下的解决方法 若有不

【Java并发编程】并发编程大合集-值得收藏

http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Ja

Java并发编程(一)

Java并发编程(一) 之前看<Thinking In Java>时,并发讲解的挺多的,自己算是初步了解了并发.但是其讲解的不深入,自己感觉其讲解的不够好.后来自己想再学一学并发,买了<Java并发编程实战>,看了一下讲的好基础.好多的理论,而且自我感觉讲的逻辑性不强.最后,买了本<Java并发编程的艺术>看,这本书挺好的,逻辑性非常强. 1. 概述 本篇文章主要内容来自<Java并发编程的艺术>,其讲解的比较深入,自己也有许多不懂的地方,然后自己主要把它讲

【Java并发编程】并发编程大合集

转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Java并发编程]Volatile关键字(上)