Java 多线程 相关概念

前言

本篇文章介绍一些多线程的相关的深刻概念。理解后对于线程的安全性会有更深的理解。html

先说一个格言,摘自Java核心技术:
若是向一个变量写入值,而这个变量接下来可能会被另外一个线程读取;或者一个变量读值,而这个变量多是以前被另外一个线程写入的,此时必须同步。java

下面就是概念了。程序员

1. Monitor机制:

  • Monitor实际上是一种同步工具、同步机制,一般被描述成一个对象,主要特色是:编程

    1. 同步。
      对象的全部方法都被互斥的执行。比如一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都须要得到这个“许可”,离开时把许可归还。
    2. 协做。
      一般提供signal机制。容许正持有许可的线程暂时放弃许可,等待某个监视条件成真,条件成立后,当前线程能够通知正在等待这个条件的线程,让它能够从新得到运行许可。
  • 在 Monitor Object 模式中,主要有四种类型参与者:数组

    1. 监视者对象 Monitor Object
      负责公共的接口方法,这些公共的接口方法会在多线程的环境下被调用执行。
    2. 同步方法
      这些方法是监视者对象所定义。为了防止竞争条件,不管是否有多个线程并发调用同步方法,仍是监视者对象还用多个同步方法,在任一事件内只有一个同步方法可以执行。
    3. 监控锁 Monitor Lock
      每个监视者对象都会拥有一把监视锁。
    4. 监控条件 Monitor Condition
      同步方法使用监视锁和监视条件来决定方法是否须要阻塞或从新执行。
  • Java中,Object 类自己就是监视者对象,Java 对于 Monitor Object 模式作了内建的支持。缓存

    • Object 类自己就是监视者对象
    • 每一个 Object 都带了一把看不见的锁,一般叫 内部锁/Monitor 锁/Instrinsic Lock, 这把锁就是 监控锁
    • synchronized 关键字修饰方法和代码块就是同步方法
    • wait()/notify()/notifyAll() 方法构成监控条件(Monitor Condition)

2. 内存模型

Java的并发采用的是共享内存模型,线程间通讯是隐式的,同步是显示的;而咱们在Android中所常说的Handler通讯即采用的是消息传递模型,通讯是显示的,同步是隐式的。安全

  • 并发编程模型的分类
    并发编程中,须要处理两个问题:线程之间如何通讯、线程之间如何同步。bash

    • 通讯是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通讯机制有两种:共享内存和消息传递。
      在共享内存的并发模型里,线程之间经过写-读内存中的公共状态来隐式进行通讯;而在消息传递模型里,线程之间没有公共状态,必须经过明确的发送信息来显示进行通讯。
    • 同步是指程序用于控制不一样线程之间操做发生相对顺序的机制。
      在共享内存并发模型里,同步是显示进行的,程序员必须显示指定某段代码或方法须要在线程间互斥执行;而在消息传递模型中,因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。
  • Java内存模型的抽象
    Java堆内存在线程间共享,下文所说的共享变量即被存储在堆内存中变量:实例域、静态域和数组。局部变量、方法定义参数和异常处理参数不会在线程之间共享,不会有内存可见性问题,也不受内存模型影响。多线程

  • Java线程之间的通讯由Java内存模型(JMM,Java Memory Module)控制,JMM决定了一个线程对共享变量的写入什么时候对另外一个线程可见。
    JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,也叫工做内存,本地内存中存储了该线程以读/写共享变量的副本。(本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。)
    因此线程A和线程B要通讯步骤以下:并发

    1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去
    2. 而后,线程B到主内存中去读取线程A以前已更新过的共享变量
  • 线程模型图

    线程模型
    线程模型

3. 原子性

原子性指:一个操做(有可能包含有多个子操做)要么所有执行(生效),要么所有都不执行(都不生效)。
java.util.concurrent.atomic包中不少类使用了CAS指令来保证原子性,而再也不使用锁。如AtomicIntergerAtomicBooleanAtomicLongAtomicReference等。
原子性不保证顺序一致性,只保证操做是原子的。

4. 内存可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程可以当即看到。

  • 从上面可知道线程模型,线程a对共享变量修改时先把值放到本身的工做内存中,而后再把工做内存中的共享变量更新到主内存中;线程b一样如此;当线程a更新了主内存后线程b刷新工做内存后就能看到a更新后的最新值。这就是内存可见性问题。
  • 内存可见性要保证两点:
    1. 线程修改后的共享变量更新到主内存;
    2. 从主内存中更新最新值到工做内存中;

5. happens-before

happens-before规则对应于一个或多个编译器和处理器重排序规则,对于程序员来讲,该规则易懂,避免为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。

使用happens-before的概念来阐述操做之间的内存可见性
若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关系。
这两个操做能够在一个线程内,也能够是不一样线程。

两个操做之间具备happens-before关系,并不意味着前一个操做必需要在后一个操做前执行;仅仅要求前一个操做的执行结果对后一个可见,且前一个操做按顺序排在第二个操做以前。

  • 传递规则:若是操做1在操做2前面,而操做2在操做3前面,则操做1确定会在操做3前发生。该规则说明了happens-before原则具备传递性
  • 锁定规则:一个unlock操做确定会在后面对同一个锁的lock操做前发生。这个很好理解,锁只有被释放了才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操做先发生于后面对该变量的读操做
  • 程序次序规则:一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start()方法先发生于此线程的其它动做
  • 线程终结原则:线程的终止检测后发生于线程中其它的全部操做
  • 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象构造先于它的finalize发生

6. CAS指令

是现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子方式对内存执行读-写-改操做,这是在多处理器中实现同步的关键。
AtomicIntergerAtomicBooleanAtomicLong的实现都是基于CAS指令。

7. 重排序

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽量的提升开发并行度。

  • 编译器和处理器会对指令进行重排序以提升性能,重排序有三种类型:
    1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。
    2. 指令级别的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。若是不存在数据依赖,处理器能够改变语句对应机器指令的执行顺序。
    3. 内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是乱序执行。
  • 这些重排序均可能会致使多线程程序出现内存可见性问题。
    对于处理器重排序,JMM会要求编译器在生成指令序列时插入特定类型的内存屏障指令来禁止特定类型的处理器重排。
    JMM属于语言级别的内存模型,它确保在不一样的编译器和不一样的处理器平台上,经过禁止一些重排序问题来保证内存可见性。
  • as-if-serial语义
    是指无论怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。
    因此,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。

  • 数据依赖性

    • 有三种类型:
      1. 写后读:a = 1; b = a;
      2. 写后写:a = 1; a = 2;
      3. 读后写:a = b; b =1;
    • 举个例子:
      int a = 1; int b = 1; int sum = a + b;
      A和B不存在数据依赖,sum却依赖A和B。因此执行顺序多是ABsum,也多是BAsum。
  • 重排序对多线程的影响
    重排序破坏了多线程程序的语义。对于存在控制依赖的操做(if语句)进行重排序,由于单线程程序是按顺序来执行的,因此执行结果不会改变;而多线程程序中,重排序可能会改变运行结果。
    对控制依赖if(flag){b = a*a}的重排序以下,编译器和处理器会采用猜想执行来克服相关性来对并行度的影响,对先提取并计算a*a,而后把计算结果保存到名为重排序缓冲的硬件缓存中,接下来再判断flag是否为真。另外一个线程设置为true了,并设置a=1,然而取得的值可能为0,与预期不符。这就是影响的一个案例。

  • 重排序的一个示例,摘自EffectiveJava:

    while(!done) {
      i++
    }
    //重排后。这种优化称做提示,是HopSpot Server VM的工做
    if(!done){
      while(true) {
        i++;
      }
    }复制代码

8. 顺序一致性

若是一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。JMM对正确同步的多线程程序的内存一致性作了以下保证:若是程序是正确同步的,程序的执行将具备顺序一致性,即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

  • 顺序一致性内存模型(为程序员提供了极强的内存可见性保证)的两大特性:
    1. 一个线程中的全部操做必须按照程序的顺序来执行
    2. 全部线程都只能看到一个单一的操做执行顺序。每一个操做都必须原子执行且马上对全部线程可见。
  • 其中对顺序一致性和原子性的区别
    原子性保证操做的原子性,而不是顺序的一致性。

9. volatile域

首先要明确,线程的安全性须要三点保证:原子性、可见性,顺序性。只有知足了这三个条件时线程才是安全的。

  • 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:
    1. 保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。
      volatile 变量保证的是一个线程对它的写会当即刷新到主内存中,并置其它线程的副本为无效,它并不保证对 volatile 变量的操做都是具备原子性的。
    2. 禁止进行指令重排序。
  • synchronized、Lock彻底保证了这三点;volatile仅保证了可见性和顺序性(禁止指令重排),在某些状况下可使用volatile代替synchronized以提升性能。在这种状况下,volatile是轻量级的synchronized。

    • 某些状况下是指:
      假设对共享变量除了赋值之外并不完成其余操做,那么能够将这些共享变量声明为volatile。即共享变量自己的操做是原子性的、顺序性的,只缺可见性了,此时能够用volatile关键字。在使用时要仔细分析。
      具体是指:

      • 对变量的写操做不依赖于当前值。
      • 该变量没有包含在具备其余变量的不变式中。
    • 要记住,原子性指的是对共享变量的操做(包括其子操做,即多条语句)是一块的,要么执行,要么不执行。不是说用了AtomicInteger就是原子性的,而是对AtomicInteger这个共享变量的操做是否是多条语句,这些多条语句是否是原子性的。

  • 经典示例1:单例模式

  • 经典示例2:

    boolean volatile isRunning = false;
    public void start () {
    new Thread( () -> {
      while(isRunning) {
        someOperation();
      }
    }).start();
    }
    public void stop () {
    isRunning = false;//只有赋值操做,非多条语句
    }复制代码


参考:
Java进阶(二)当咱们说线程安全时,到底在说什么
Java并发编程:volatile关键字解析
并发模型——共享内存模型(线程与锁)理论篇
《深刻理解Java内存模型》

相关文章
相关标签/搜索