Java多线程和内存可见性

什么是进程

  • 程序(任务)的执行过程。
  • 持有资源(共享内存,共享文件)和线程(能够看作为载体)。

什么是线程

  • 线程是系统中最小的执行单元。
  • 同一个进程中有多个线程。
  • 线程共享进程的资源

线程的交互

  • 互斥
  • 同步

经常使用方法

  • Thread类经常使用的方法java

    • 线程的建立缓存

      • Thread()
      • Thread(Stirng name)
      • Thread(Runnable target)
      • Thread(Runnable target,String name)
    • 线程的方法安全

      • void start() 用于启动线程
      • 线程休眠多线程

        • static void sleep(long millis)
        • static void sleep(long millis,int nanos)
      • 使其余线程等待,当前线程终止并发

        • void join()
        • void join(long millis)
        • void join(long millis,int nanos)
        • static void yield() 当前运行线程释放处理器资源
    • 获取线程引用函数

      • static Thread currentThread() 返回当前运行的线程引用

可见性

  • 可见性:
一个线程对共享变量值的修改,可以及时地被其余线程看到
  • 共享变量:
若是一个变量在多个线程的工做内存中都存在副本,那么这个变量就是这几个线程的共享变量。

Java内存模型(JMM)

Java内存模型(Java Memory Model)描述了Java程序中各类变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
image
  • 全部的变量都存储在主内存中
  • 每一个线程都有本身独立的工做内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 两条规定高并发

    • 线程对共享变量的全部操做都必须在本身的工做内存中进行,不能直接从主内存中读写
    • 不一样线程之间没法直接访问其余线程工做内存中的变量,线程间变量值的传递须要功过主内存来完成。
  • 共享变量可见性实现的原理
线程1对共享变量的修改要想被线程2及时看到,必需要通过以下的两个步骤。

1.把工做内存1中更新过的共享变量刷新到主内存中性能

2.把内存中最新的共享变量的值更新到工做内存2中优化

synchronized实现可见性

synchronized可以实现:

  • 原子性(同步)
  • 可见性

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁时,将清空工做内存中共享变量的值,从而使用共享变量时须要从主内存中从新读取最新的值(注意:加锁与解锁须要的是同一把锁)

这两点结合起来,就能够保证线程解锁前对共享变量的修改在下次加锁时对其余的线程可见,也就保证了线程之间共享变量的可见性。ui

线程执行互斥代码的过程:

  1. 得到互斥锁
  2. 清空工做内存
  3. 从主内存拷贝最新副本到工做内存中。
  4. 执行代码
  5. 将更改事后的共享变量的值刷新到主内存中去。
  6. 释放互斥锁。

指令重排序

重排序:代码书写的顺序与实际执行的顺序不一样,指令重排序是编译器或处理器为了提供程序的性能而作的优化
  1. 编译器优化的重排序(编译器优化)
  2. 指令级并行重排序(处理器优化)
  3. 内存系统的重排序(处理器优化)

as-if-serial

as-if-serial:不管如何重排序,程序执行的结果应该和代码顺寻执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)

例子:

int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行
  • 单线程:第一行和第二行能够重排序,但第三行不行
  • 重排序不会给单线程带来内存可见性问题
  • 多线程中程序交错执行时,重排序可能会照成内存可见性问题。

可见行分析:

致使共享变量在线程间不可见的缘由:

  1. 线程的交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在工做内存与主内存间及时更新

volatile实现可见性

volatile关键字:

  • 可以保证volatile变量的可见性
  • 不能保证volatile变量的原子性

volatile如何实现内存可见性:

深刻来讲:经过加入内存屏障和禁止重排序优化来实现的。
  • 对volatile变量执行写操做时,会在写操做后加入一条store屏障指令

    • store指令会在写操做后把最新的值强制刷新到主内存中。同时还会禁止cpu对代码进行重排序优化。这样就保证了值在主内存中是最新的。
  • 对volatile变量执行读操做时,会在读操做前加入一条load屏障指令

    • load指令会在读操做前把内存缓存中的值清空后,再从主内存中读取最新的值。

volatile如何实现内存可见性:

通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当变量发生变化时,又强迫线程将最新的值刷新到主内存。这样任什么时候刻,不一样的线程总能看到该变量的最新的值。

线程写volatile变量的过程:

  1. 改变线程工做内存中volatile变量副本的值。
  2. 将改变后的副本的值从工做内存刷新到主内存。

线程读volatile变量的过程:

  1. 从主内存中读取最新的volatile变量的值到工做内存中。
  2. 从工做内存中读取volatile变量的副本。

volatile不能保证volatile变量复合操做的原子性:

private int number=0;//原子性操做
number++;//不是原子性操做

number++的步骤能够分为三步:

  1. 读取number的值
  2. 将number的值加1
  3. 写入最新的number的值

若是使用synchronized关键字:

//加入synchronized关键字后能够number++的步骤变成原子操做。
synchronized(this){
    number++;
}

加入synchronized关键字后能够number++的步骤变成原子操做。

保证方法操做的原子性

解决方案:

  • 使用synchronized关键字
  • 使用ReentrantLock(java.until.concurrent.locks包下)
  • 使用AtomicInterger(vava,util.concurrent.atomic包下)

volatile的适用场景

要在多线程总安全的使用volatile变量,必须同时知足:

  1. 对变量的写入操做不依赖其当前值

    • 不知足:number++、count=count*5
    • 知足:boolean变量、记录温度变化的变量等
  2. 该变量没有包含在具备其余变量的不变式中

    • 不知足:不变式 low<up

synchronized和volatile的比较;

  • synchronized锁住的是变量和变量的操做,而volatile锁住的只是变量,并且该变量的值不能依赖它自己的值,volatile算是一种轻量级的同步锁
  • volatile不须要加锁,比synchronized更加轻量级,不会阻塞线程。
  • 从内存可见性角度讲,volatile读至关于加锁,volatilexie至关于解锁。
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,没法保证原子性。
注:因为voaltile比synchronized更加轻量级,因此执行的效率确定是比synchroized更高。在能够保证原子性操做时,能够尽可能的选择使用volatile。在其余不能保证其操做的原子性时,再去考虑使用synchronized。

补充内容

  1. 问:即便没有保证可见性的措施,不少时候共享变量依然可以在主内存和工做内存中及时的更新
通常只有在短期内高并发的状况下才会出现变量得不到及时更新的状况,由于cpu在执行时会很快的刷新缓存,因此通常状况下很难看到这种问题。
  1. 对64位(long、double)变量的读写可能不是原子操做:
Java内存模型容许JVM将没有被volatile修饰的64位数据类型读写操做划分为两次32位的读写操做来进行,这就会致使有可能读取到“半个变量”的状况,解决办法就是加上volatile关键字。
  1. final也能够保证线程之间内存变量的可见性。
Final 变量在并发当中,原理是经过禁止cpu的指令集重排序,具体能够在 重排序详解1重排序详解2,来提供现成的可见性,来保证对象的安全发布,防止对象引用被其余线程在对象被彻底构造完成前拿到并使用。

与锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵照两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。
与Volatile 有类似做用,不过Final主要用于不可变变量(基本数据类型和非基本数据类型),进行安全的发布(初始化)。而Volatile能够用于安全的发布不可变变量,也能够提供可变变量的可见性。
相关文章
相关标签/搜索