15

什么是进程

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

什么是线程

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

线程的交互

  • 互斥
  • 同步

常用方法

  • Thread类常用的方法

    • 线程的创建

      • 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的两条规定:

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

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

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

  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可以用于安全的发布不可变变量,也可以提供可变变量的可见性。

凉皮
74 声望9 粉丝

引用和评论

0 条评论