文章目录

  • Java并发知识以及volatile关键字
  • 临界资源和临界区
  • 临界资源
  • 临界区
  • 空闲让进
  • 忙则等待
  • 有限等待
  • 让权等待
  • 线程安全
  • 并发特性
  • 原子性
  • 可见性
  • 有序性
  • Volatile关键字
  • JMM-Java内存模型
  • volatile工作原理
  • 1.**保证内存可见性**
  • 2.**禁止指令重排序**
  • happen-before
  • 注意:
  • volatile满足原子性吗:
  • volatile修饰对象是否起作用:
  • 主内存和工作内存是如何交互的?


Java并发知识以及volatile关键字

Java并发的重要性在于最大限度提高计算资源的效率。

并发是多线程中交替的访问同一个资源,同一个资源可以实CPU资源,可以是内存资源,该资源的特点只能同一时刻只能一个线程进行访问。

临界资源和临界区

临界资源

一般指内存资源,一个时刻只能有一个线程进行访问,一个线程正在使用临界资源的时候,另一个线程是不能使用,临界资源是非可剥夺资源。即JVM也无法阻止这种资源的独占行为

临界区

是一个线程中访问 临界资源程序片段,不是内存资源。这就是临界区和临界资源的区别。

临界区使用原则: 空闲让进 ,忙则等待 , 有限等待 , 让权等待

空闲让进

临界资源空闲一定让线程进入,不发生“互斥礼让”行为 。

忙则等待

临界资源正在使用时,外面的线程就需要等待

有限等待

线程进入临界区的时间时有限的,不会发生“饿死”的情况。

饿死:进入到临界区的线程,有一定时间限制,不可能让此线程一直执行下去,如果一直执行,那么其他需要等待此资源的线程就会一直等待下去,执行不了 即为 饿死现象。

让权等待

线程不能进入到临界区时 应该让出CPU的使用,一面进程陷入忙等状态

线程安全

在单线程下和多线程执行下,最终得到的结果是相同的。

并发特性

Java的并发模型中围绕并发过程中如何处理原子性,可见性,有序性问题

原子性

如果一个操作是不可分割的,即称之为原子性。

int a = 10; //1 10赋值给线程工作内存中的变量a 原子操作

a++; //2 拿a 进行a+1 赋值a 非原子操作

int b = a; //3 拿a b=a 非原子操作

a = a+1; //4 拿a 进行a+1 赋值a 非原子操作

可见性

一个变量被多个线程访问,如果一个线程改变了这个变量,其他线程能立即看到此修改,称之为可见性

有序性

Java中的有序性分为两个方面

线程内部观察:所有操作时有序的,其中一个线程的所有操作都是有序的“串行”(as-if seial 像排序一样)

线程间观察: 在某一个线程中观察另一个线程,所有线程都是并行执行 交叉执行。

Volatile关键字

多线程情况下 : 当变量加上volatile关键字 后,变量对所有线程可见

特征

  1. 保证内存可见性
  2. 禁止指令重排序

那么 为什么不加volatile的变量其他线程不可见?

这就要说到Java内存模型(JMM) 请看下图:

JMM-Java内存模型

java内存模型是共享内存并发模型线程之间主要通过读-写共享变量来完成隐式通信。java中的共享变量是存储在内存中的,多个线程由其工作内存,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。

在Java内存模型上,Java堆内存主要保存对象和基本数据类型的备份,称为主内存Java栈中保存的变量的部份内存,称为本地内存(工作内存)。

而在下图可知:

java 临界区内 java中临界区用什么关键字_java 临界区内

  1. 每个线程再对变量的任何操作都是再自己的本地内存中进行,它是不会直接操作到主内存中的变量。
  2. 不同的线程是无法访问或者获取不同线程的本地内存的变量。
  3. 而线程之间变量的传递主要是通过主内存来完成,

而主内存和本地内存之间的交互是遵循具体的交互协议,从工作内存到主内存 主内存到工作内存

交互协议主要有8种:Lock 锁,unlock 解锁,read 读取,load 载入,use 使用,assign 赋值,store 存储,write 写入。

这些操作遵循原子性

这就是不加volatile其他线程不可见的原因。

看一个例子:

此例子中是要计算一秒中count++的次数

class volatileText{
    private static  boolean flag = true; //设置一个标志位

    public static void main(String[] args) {
        //线程A 进行count的计数
      Thread A = new Thread(new Runnable() {
          @Override
          public void run() {
              int count = 0;
              while (flag){
                  count++;
              }
              System.out.println("Count:" +count);
          }
      });
      //线程B进行休眠1秒 然后设置标志位flag为false。计算1秒会计数多少次
      Thread B = new Thread(new Runnable() {
          @Override
          public void run() {
              try {
                  Thread.sleep(1000);//休眠一秒
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              flag = false; //将标志位置为false
              System.out.println("1min后结束");
          }

      });
      A.start();
      B.start();
    }
}

首先来说结果,在flag没有加Volatile发现程序是无法结束的,这是不确定是否执行结束的

java 临界区内 java中临界区用什么关键字_多线程_02

原因就是A线程无法获取flag变量的改变。

进行分析一哈:首先flag = true 变量在主内存中,线程A要获取 flag ,通过交互操作获取的是在本地内存A flag 的副本,而线程B在睡眠一秒后,开始获取 flag 的值 进行修改,而B线程也是从它相应的 本地内存B中 获取 flag 进行修改 false。

原因就在这: 线程B 修改的是本地内存B 中的flag 副本并没有修改主内存中的flag,而 线程B没有及时的写入到主内存中或者主内存没有及时的读取本地内存B 中的 flag, 然而 线程A或者 主内存没有主动交互跟新flag的值,所以 线程A没有获取到最新的值继而一直执行下去。

而加上 volatile

private static volatile boolean flag = true; //设置一个标志位

java 临界区内 java中临界区用什么关键字_Java_03

可以看到线程执行结束。

底层原理:在汇编层面(非字节码 产生的.class)文件 ,可以看到在对应的汇编语句前加入了“Lock”(相当于内存屏障(栅栏)),当B线程修改flag变量操作时:

  1. 本地内存B将flag副本修改位最新值,并立即将最新值写到主内存中,通过总线将A线程的flag副本的标志(这里的标识是在底层每个变量都会有一个标识)置为无效。
  2. 当线程A访问flag的副本时,会先检查副本的标志位,若为无效,则不会读取副本中的值,而是会将主内存的最新值拷贝到本地内存副本中。

volatile工作原理

加了volatile的变量,在底层汇编层面 代码会多一个Lock前缀指令,相当一个内存屏障(栅栏)

而它的作用:

  1. 它保证在重排序的时候不会将其后面的指令排到内存屏障前的位置,也不会将内存屏障前的指令排到内存指令之后
  2. 它会强制将工作内存中的数据立即写回到主内存。
  3. 如果是 写操作,它会立即导致其他CPU的对应的工作内存(缓存)立即无效

看到这后再来说说volatile的特征

1.保证内存可见性

volatile修饰的变量副本(在本地内存中存储变量副本:Java虚拟机栈/寄存器(只能存储很少的变量,目的还是提高效率)) 它不会缓存到寄存器中,加了volatile后,只会储存到本地内存(虚拟机栈线程私有的空间),一旦变量修改就会立即写回到主内存,每一个线程访问主内存上数据都是最新的变量。如果此个变量是多个线程共享的变量,那么其他线程的本地副本变量标志位会置为无效,从而其他线程会进入主内存重新获取最新的变量值。

2.禁止指令重排序

Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序。

这里要清楚一件事:我们平时写的源码都会保存到 .java文件中 操作系统进行执行时会 转换为 .class 文件,而.class 文件还会再汇编层次再进行优化(全部转为二进制),目的还是为了提高效率。而汇编层次的优化请看下面的例子:

java 临界区内 java中临界区用什么关键字_java_04

a = 10;
a++; 在汇编层面优化: a = 11;
b = 12; b = 12;

从结果来看 三行代码 优化为 两行 提高了效率。 从而可以看到 在优化前后数据最终结果保持不变的前提下,底层操纵系统会进行优化。但是在多线程的情况下,不能保证代码的排序,按单线程情况下排序是没有问题的,但是在多线程情况下就会出现两个线程所看到的结果不一致的问题,所以会加volatile后 禁止指令的重排序

而重排序又遵循happen-before原则,它是操作系统设计之初就制定的规则。

happen-before

happen-before是JMM最核心的概念

Java内存模型(JMM)可以通过happen-before原则在多线程条件下保证了内存的可见性和操作的可见性。

具体的规则:
(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

 

注意:

volatile满足原子性吗:

volatile满足可见性,不保证原子性,也不能保证线程安全。有序性存在争议。

底层来说 它是保证有序性的,但是往大来说它又不满足有序性。

volatile修饰对象是否起作用:

volatile只能修饰变量,对基本数据类型起作用。

volatile修饰对象不起作用,只能保证对对象的地址空间进行可见,如果地址空间发生改变,其他线程能立即可见,但是如果对象本身的属性发生改变,volatile不能保证其他线程能立即可见。

主内存和工作内存是如何交互的?

java 临界区内 java中临界区用什么关键字_多线程_05

如果volatile当前修饰的是一个变量

  1. 变量值从主内存(堆中)加载load到本地内存(虚拟机栈的栈帧中)。
  2. 线程对该变量的操作就在工作内存中,作用在副本中,在这之后如果主内存或者副本的数据发生改变都不互相联系,这就导致线程不安全,存在数据不一致的情况。
  3. 如果volatile修饰的变量在某线程中发生改变,会立即将该变量的修改写入到主内存中,其他线程的副本会立即失效,然后会重新拷贝主内存的最新值。·