对于volatile和synchronized的一些整理
Posted AJINGWOMAN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对于volatile和synchronized的一些整理相关的知识,希望对你有一定的参考价值。
对于volatile和synchronized的一些整理
目录
精确地说就是编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
最后的补充:volatile和synchronized的有序性为什么不一样
从单例类引入
这周遇到了关于单例类的书写问题
发现懒汉式单例类具有线程不安全的问题
单例类主要分饿汉式单例类和懒汉式单例类
他们主要的区别就是 饿汉式单例类是空间换时间 懒汉式单例类是时间换空间
饿汉式单例类
/*饿汉式是典型的空间换时间
*类初始化时创建单例,线程安全,
*适⽤于单例占内存⼩的场景,
*否则推荐使⽤懒汉式延迟加载*/
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
//静态工厂方法
public static EagerSingleton getInstance() {
return instance;
}
private EagerSingleton(){}
}
懒汉式单例类
/*懒汉式是典型的时间换空间
*需要创建单例实例的时候再创建,
* 需要考虑线程安全(性能不太好)*/
public class LazySingleton {
private static LazySingleton instance = null;
// 静态工厂方法
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
private LazySingleton() {
}
}
*问题:线程不安全!
解决方法之一:双重加锁
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if(instance==null){
//volatile关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
instance = new Singleton();
}
}
}
return instance;
}
//volatile关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
instance = new Singleton();
//instance=new Singleton语句为非原子性,实际上会执行以下内容:
//(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
//假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),当单例模
式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;但在多线程情况
下,假如线程1执行instance=new Singleton()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤
2,但此时已有引用指向对象也就是instance!=null,故线程2在第一次检查时不满足条件直接返回
instance,此时instance为null(即str值为null)
private Singleton() {
}
}
抛出问题:
1.volatile是什么?作用是什么?
2.非原子性?禁止指令重排?
3.volatile怎么用?
volatile定义与作用
百科:
volatile是一个特征修饰符volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
精确地说就是,编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
抛出问题:
编译器?寄存器?
内存模型?
内存可见性
内存模型
定义:
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
讲人话:
简单来说就是java的多线程之间内存处理的一个规定
JMM(Java Memory Model)具体规定 :
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存(高速缓存),保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
简单来说就是:东西只有一份 你们不可以直接拿 你们先复印一下 在自己的复印件上面动手脚 并且你们还不能互相抄作业
为什么?
为了解决:
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
举个例子:i=i+1
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
但是多线程之中
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
多线程中出现了缓存不一致!
总结:
由于JMM内存模型的原因,多线程中可能会出现缓存不一致的情况,解决这种问题就是所谓的保证了内存可见性
内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个
变量变化。也就是说,如果线程 A 修改了共享变量 X 的值,那么线程 B 在使用 X 的值时,能立即读到 X 的最新值。
解决:加锁或者是加volatile
总结:所以volatile的作用之一就是保证内存的可见性👇
精确地说就是编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。
对volatile变量执行读操作时,会在读操作后加入一条load屏障指令。
而synchronized也同样保证了内存的可见性
寄存器
百科:
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
按照功能的不同,可将寄存器分为基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据,也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输 入、 并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。
原子性:
原子性(atomicity)是2018年公布的计算机科学技术名词。
指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
人话:多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
例:
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。
- X=10; //原子性(简单的读取、将数字赋值给变量)
- Y = x; //不是原子操作,变量之间的相互赋值
- X++; //不是原子操作,对变量进行计算操作
- X = x+1;
语句2实际包括两个操作,它先要去读取x的值,再将y值写入,两个操作分开是原子性的。合在一起就不是原子性的。
语句3、4:x++ x=x+1包括3个操作:读取x的值,x+1,将x写入
补充:创建一个对象的过程
1、在堆区分配对象需要的内存
分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
2、对所有实例变量赋默认值
将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值
3、执行实例初始化代码
初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法
4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它
需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super关键字来调用父类对象,但在外部不可访问
总结:原子性
instance = new Singleton();为非原子性的
(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
可以通过 synchronized和Lock实现更大范围的原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
补充:lock是一个接口 大致用法
@Override
public void run() {
// TODO Auto-generated method stub
lock.lock();
try {
Thread.sleep(1000);
System.out.println("goon");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
有序性
人话:
即程序执行的顺序按照代码的先后顺序执行
java指令重排序
为了性能优化, 为了进一步提升计算机各方面能力
在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题
编译器和处理器会进行指令重排序
多条汇编指令执行时, 考虑性能因素, 会导致执行乱序
指令重排只可能发生在毫无关系的指令之间, 如果指令之间存在依赖关系, 则不会重排
总结:有序性
所以
instance = new Singleton();的三个操作可能被重新排序
所以线程2在读取数据的时候 线程1可能已经引用指向对象了 但是还没有初始化数据
所以返回的还是null
所以volatile具有禁止指令重排序的作用, 其具有有序性。
回头看单例类
假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B)。线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。
这个过程看上去是不是无懈可击,没有漏洞?
答案是否定的,问题就出在了new操作上,我们以为的new操作是这样的:
1.分配一块内存空间
2.在这块内存空间上初始化Singleton实例对象
3.把这个对象的内存地址赋值给instance变量
但实际上由于指令重排,优化后的过程是这样的:
1.分配一块内存空间
2.把这快内存空间的内存地址赋值给instance变量
3.在这块内存空间上初始化Singleton实例对象
那么这样调换顺序后会发生什么呢?
我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
volatile与synchronized区别
- 仅靠volatile不能保证线程的安全性。(原子性)
- volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
- volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
- synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
总结:volatile和synchronized
线程安全三大特性:可见性 原子性 有序性
synchronized: 具有原子性,有序性和可见性;
volatile:具有有序性和可见性
单例类中双重加锁的原因
synchronized保证了创建对象操作的原子性 即一次进入一个线程
volatile保证了变量instance的可见性 也就是所有的操作都能实时被读取
而volatile禁止了instance = new Singleton();的指令重排序 读取---->数值初始化---->引用对象
最后的补充:volatile和synchronized的有序性为什么不一样
volatile禁止指令重排
synchronized不能禁止指令重排 却保证了有序性
怎么回答:
- 为了进一步提升计算机各方面能力,在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题。
- 我们也知道,最好的解决有序性问题的办法,就是禁止处理器优化和指令重排,就像volatile中使用内存屏障一样。
- 但是,虽然很多硬件都会为了优化做一些重排,但是在Java中,不管怎么排序,都不能影响单线程程序的执行结果。这就是as-if-serial语义,所有硬件优化的前提都是必须遵守as-if-serial语义。
- 再说下synchronized,他是Java提供的锁,可以通过他对Java中的对象加锁,并且他是一种排他的、可重入的锁。
- 所以,当某个线程执行到一段被synchronized修饰的代码之前,会先进行加锁,执行完之后再进行解锁。在加锁之后,解锁之前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。
- synchronized通过排他锁的方式就保证了同一时间内,被synchronized修饰的代码是单线程执行的。所以呢,这就满足了as-if-serial语义的一个关键前提,那就是单线程,因为有as-if-serial语义保证,单线程的有序性就天然存在了。
最后的最后的补充:volatile和内存屏障
内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
- 在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。
- 在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。
- 在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。
- 在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。
引用文章
https://baike.baidu.com/item/volatile/10606957?fr=aladdin
https://zhuanlan.zhihu.com/p/138819184
https://baike.baidu.com/item/%E5%8E%9F%E5%AD%90%E6%80%A7/7760668?fr=aladdin
https://blog.csdn.net/tpriwwq/article/details/102484781
https://www.cnblogs.com/czwbig/p/11127124.html
https://www.jianshu.com/p/cf57726e77f2
https://www.cnblogs.com/xdecode/p/8948277.html
https://blog.csdn.net/a347911/article/details/88379625
https://blog.csdn.net/qq_37335220/article/details/89597489
https://blog.csdn.net/onroad0612/article/details/81382032
以上是关于对于volatile和synchronized的一些整理的主要内容,如果未能解决你的问题,请参考以下文章
多线程---再次认识volatile,Synchronize,lock
Java 虚拟机:互斥同步锁优化及synchronized和volatile
Java虚拟机:十七互斥同步锁优化及synchronized和volatile