Java多线程(11)——volatile详解

  • Post author:
  • Post category:java


1 基本概括

2 主要介绍


2.1 Java内存模型(JMM)

JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。


Java内存模型

(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。


所有的共享变量都存储于主内存

。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。


2.1.1 Java内存模型以及操作规范变量

1.共享变量必须存放在主存中;

2.线程有自己的工作内存,线程只可操作自己的工作内存;

3.线程要操作共享变量,需从主存中读取到工作内存,改变之后需从工作内存同步到主存中。

Java内存模型会带来什么问题?

有变量A,多线程并发对其累加会有什么问题?如三个线程并发操作变量A,大家读取A时读到A=0,

都对A+1,在将值同步会主内存,结果是多少?

结果是1.

这就是

线程安全问题

如何解决线程安全问题?

内存模型也产生了变量可见性问题。

如何让线程2使用A时看到最新值?

1.线程1修改A后必须马上同步回主内存

2.线程2使用A前需从主内存读取到工作内存。

疑问一:使用前不会重新从主内存读取到工作内存吗?

疑问二:修改后不会立马同步会主内存吗?


2.1.2.Java内存模型–同步交互协议


规定了8种原子操作:

1.

lock

(锁定):将主内存中的变量锁定,为一个线程所独占

2.

unlock

(解锁):将lock加的锁定解除,此时其它的线程可以有机会访问此变量

3.

read

(读取):作用于主存变量,将主存中的变量读到工作内存当中

4.

load

(载入):作用于工作内存变量,将read读取的值保存到工作内存中的变量副本中

5.

use

(使用):作用于工作内存变量,将值传递给线程的代码执行引擎

6.

assign

(赋值):作用于工作内存变量,将执行引擎处理返回的值重新赋值给变量副本

7.

store

(存储):作用于工作内存变量,将变量副本的值传送到主内存中

8.

write

(写入):作用于主内存变量,将store传送过来的值写入到主内存的共享变量中


操作规范

1.将一个变量从主内存复制到工作内存要顺序执行read load操作;要将变量从工作内同步回主内存要顺序执行store、write操作。只要求顺序执行,不一定是连续执行

2.做了assign操作,必须同步回主存。不能没做assign,同步回内存。


并发中保持变量可见性的方式:


1.final变量

 //final不可变变量 private final int var1 = 1;


2.Synchronized

 while (VisibilityDemo.is){ synchronized(this){ i++; } }


3 用volatile修饰

 //状态标识 private static volatitle boolean is =true;


2.2 多线程编程的三个概念(JMM定义)


2.2.1 原子性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。


2.2.2 可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。


2.2.3 有序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。


2.3 指令重排序


2.3.1 什么是指令重排序

执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序

编译器优化的重排序,在不改变单线程语义的情况下重新安排语句的执行顺序

指指令级并行重排序,处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性将会改变对应机器指令的执行顺序

内存系统的重排序,因为使用了读写缓存区,使得看起来并不是顺序执行的


例子如下:

//section1
int a = 1;
int b = 1;
a=a+1;
b=b+1;
//section2
int a = 1;
a=a+1;
int b = 1;
b=b+1;

第一部分的代码可能就不会有部分2的性能高,因为a可以直接从寄存器中取,不需要反复地拿a。定义完a之后马上就可以进行add操作


2.3.2 产生的问题

重排序可能会导致多线程程序出现内存可见性问题。(工作内存和主内存,编译器处理器重排序导致的可见性)

重排序会导致有序性问题,程序的读写顺序与内存的读写顺序不一样(编译器处理器重排序,内存缓冲区(是处理器重排序的内容))


2.4 happens-before


2.4.1定义

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。


2.4.2 为什么需要happens-before

JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。


2.4.3 JMM提供的happens-before 保证原子性可见性和有序性

8大准则:


程序顺序原则

,一个线程内要按照代码的顺序执行,保证语义的串行性


锁规则

,解锁操作必须发生在对同一个锁的加锁操作之前


volatile规则

,对volatile修饰的变量的写操作发生在volatile变量的读操作之前。访问volatile的变量时候强制从主内存中进行读取,volatile修饰的变量被修改之后会被强制的刷新到主内存中。所以说volatile修饰的变量保证了可见性


线程启动规则

,线程的start()方法最先执行,线程A在线程B执行start()方法之前对共享变量的修改对线程B可见


线程终止规则

,线程的所有操作优于线程的终结,Thread.join()方法的作用是等待当前线程终止,线程B在终止之前修改了共享变量,执行完成后,线程B对共享变量的修改将对线程A可见


线程中断规则

,对线程interrupt中断方法的调用发生在被中断的线程检测到中断事件的发生之前


对象终结规则

,对象的构造函数执行完成先于finalize()方法


传递性

,线程A生在线程B之前,线程B发生在线程C之前,则线程A发生在线程C之前


2.4 volatile特征与原理


2.4.1 volatile内存可见性

  1. 被 volatile 修饰的元数据在线程操纵它们的时候 JMM 有两个执行机制(load、store)可以保证元数据在内存中对所有线程的可见性
  2. load 机制:在每次有线程读取元数据的时候 JMM 都会执行load 操作把当前将要被操纵的元数据的最新值更新到内存中
  3. store 机制:线程每次操作完元数据之后 JMM 都会执行 store 操作把当前操纵的元数据更新到内存中
  4. 如下图所示:


2.4.2 volatile 防止重排序原理

为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了? 答案是可以添加内存屏障。

Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。 为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

“NO”表示禁止重排序。 为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

在每个volatile写操作的前面插入一个StoreStore屏障

在每个volatile写操作的后面插入一个StoreLoad屏障

在每个volatile读操作的后面插入一个LoadLoad屏障

在每个volatile读操作的后面插入一个LoadStore屏障

需要注意的是:volatile写操作是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。


2.4 volatile为啥不能保证原子性

修改volatile变量分为四步:

1)读取volatile变量到local

2)修改变量值

3)local值写回

4)插入内存屏障,即lock指令,让其他线程可见

这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。


2.5 如何保证原子性

主要三种方式


1 synchronized同步代码块


2 cas原子类工具


3 lock锁机制


2.6 volatile的使用场景


2.6.1 volatitle的使用范围:

1.volatile只可修饰成员变量(静态的、非静态的)【全局变量】

2.多线程并发下,才需要使用它。


2.6.2 volatitle典型的应用场景:

1、只有一个修改者,多个使用者,要求保证可见性的场景

2、状态标识,如示例中介绍的标识

2、数据定期发布,都给获取者

3 常见用例


3.1 volatile的应用双重检查锁的单例模式

public class Singleton {
        private static volatile Singleton singleton;
 
        private Singleton() {
        }
 
        public static Singleton getInstance() {
            if (singleton == null) {                1
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
 
     


3.2 volatile不保证原子性例子

/**
 * 看作jmm模型
 */
class TestVoliate {
    //jmm模型,主内存值为0
    volatile int number=0;
    //线程的工作内存进行了number++
    public void numberIncrement() {
        this.number++;
    }
}
class TestvolatileIncrementError {
    public static void main(String[] args) {
        TestVoliate myDate3 = new TestVoliate();
        //开100个线程,每个线程做100次number++
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    myDate3.numberIncrement();
                }
            }, String.valueOf(i)).start();
        }
        System.out.println(Thread.currentThread().getName() + "\t final number: "+myDate3.number);
    }
}
运行结果: main	 final number: 9575

4 常见问题

1 讲讲什么是JMM
2 JMM定义了什么
3 八种内存交互操作
4 volatile为啥不能保证原子性
5 volatile怎么禁止指令重排序
6 什么指令重排序
7 如何保证原子性 
8 long和double的原子性
9 单例的8种写法和优劣势
10 Volatile在双重检查加锁的单例中的应用
11 volatile的使用场景
12 volatile与synchronized

常见出现的问题会在后面的文章讨论,一起学习的朋友可以点点

关注,会持续更新

,文章有帮助的话可以

长按点赞有惊喜!!!



收藏



转发,

有什么补充可以在下面

评论,谢谢



版权声明:本文为qq_20761351原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。