【Kill Thread Part.2-2】volatile、原子性、JMM应用
【Kill Thread Part.2-2】volatile、原子性、JMM应用
一、volatile关键字
详解:底层原理
https://zhuanlan.zhihu.com/p/133851347
1、volatile是什么
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个遍历被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。
2、volatile的不适用场合:取决于之前的状态
①a++
public class NoVolatile implements Runnable{
volatile int a;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
NoVolatile r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(r.a);
System.out.println(r.realA.get());
}
}
②依赖于前一次的值
import java.util.concurrent.atomic.AtomicInteger;
/**
* 描述: volatile不适用的情况2
*/
public class NoVolatile2 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile2();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile2) r).done);
System.out.println(((NoVolatile2) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
}
结果可能会出现true,也可能会出现false。
3、volatile的适用场合
①boolean flag
boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
/**
* 描述: volatile适用的情况1
*/
public class UseVolatile1 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new UseVolatile1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((UseVolatile1) r).done);
System.out.println(((UseVolatile1) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
}
关键在于这个复制操作和之前的值有没有关系
②刷新之前变量的触发器
4、volatile的两点作用
①可见性
读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
②禁止指令重排序优化
解决单例双重锁乱序问题
5、volatile和synchronized的关系
volatile可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
二、可见性延伸
1、能保证可见性的措施
除了volatile可以让变量保证可见性之外,synchronized、Lock、并发集合、Therad.join()和Thread.start()等都可以保证可见性。
2、对synchronized可见性的正确理解
synchronized不仅保证了原子性,还保证了可见性。
使用上面这样的方式,可以确保synchronized代码块上面的代码也是可见的。
三、原子性
1、什么是原子性
一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
为什么i++不是原子性的
但是可以用synchronized实现原子性
2、Java中的原子操作有哪些
除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作
所有引用reference的赋值操作,不管是32位的机器还是64位的机器
java.concurrent.Atomic.*包中所有类的原子操作
3、long和double的原子性
官方文档:
对于64位的值的写入,可以分为两个32位的操作进行写入,读取错误、使用volatile解决
结论:在32位上的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的。
4、原子操作 + 原子操作 != 原子操作
简单地把原子操作组合在一起,并不能保证整体依然具有原子性。
全同步的HashMap也不完全安全
四、面试常见问题一:单例模式
JMM应用实例:单例模式8种写法、单例和并发的关系
①单例模式的作用
- 节省内存和计算
- 保证结果正确
- 方便管理
②适用场景
- 无状态的工具类:比如之日工具类
- 全局信息类:比如我们在一个类上记录网站的访问次数
单例模式的8种写法
1、饿汉式(静态常量)[可用]
/**
* 描述:饿汉式(静态常量)可用
*/
public class Singleton1 {
//在类加载的时候就会把实例化
private final static Singleton1 instance = new Singleton1();
private Singleton1() {
}
private static Singleton1 getInstance() {
return instance;
}
}
2、饿汉式(静态代码块)[可用]
/**
* 描述:饿汉式(静态代码块)
*/
public class Singleton2 {
private final static Singleton2 instance;
static {
instance = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return instance;
}
}
3、懒汉式(线程不安全)[不可用]
/**
* 描述: 懒汉式(线程不安全)
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
//两个线程同时执行这行,发现为空,创建了两个实例,不符合单例。
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
4、懒汉式(线程安全)[不推荐使用]
/**
* 描述: 懒汉式(线程安全)(不推荐)
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
缺点就是效率太低了
5、懒汉式(线程不安全,同步)[不推荐]
/**
* 描述:懒汉式(线程不安全)(不推荐)
*/
public class Singleton5 {
private static Singleton5 instance;
public Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
//如果两个线程都进入了if,虽然synchonized会让一个线程等待,但是一个线程执行完创建实例之后,释放锁,另一个线程还是会创建实例
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
6、双重检查 ![推荐使用!]
/**
* 描述:双重检查(推荐面试使用)
*/
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {//类锁
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
优点:线程安全;延迟加载;效率较高;
为什么要double-check?单check行不行?把synchronized放到方法上可不可以?(性能不行)
第一个检测大大提高了效率,只有在对象没有初始化成功的那段时间,才会有线程通过第一个检测,而第二个检测保证instance只会被初始化一次。
为什么要用volatile?
- 新建对象不是原子操作
- 新建对象实际上有三个步骤
①②③步骤可能发生重排序,产生并发问题。假设线程1创建对象,然后赋值引用,还没有调用构造方法对属性进行赋值,然后线程2此时来判断,发现rs不为空,直接返回这个对象实例,就会发现对象里的属性是空的,就发生了问题。会发生空指针(NPE)的问题。使用volatile可以防止指令重排序。
7、静态内部类 [推荐用]
/**
* 描述: 静态内部类方式,可用
*/
public class Singleton7 {
private Singleton7() {
}
//懒汉
private static class SingletonInstance {
private static final Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.instance;
}
}
8、枚举 [推荐用]
/**
* 描述; 枚举单例
*/
public enum Singleton8 {
INSTANCE;
public void whatever() {
}
}
不同写法对比
- 饿汉:简单,但是没有lazy loading
- 如果是对象的创建需要配置文件就不适用
- 懒汉:有线程安全问题
- 如果一开始要加载的资源太多,就用懒加载
- 静态内部类:可用
- 双重检查:面试用
- 枚举:实际开发最好
- 枚举类反编译发现是是静态的变量,然后还是懒加载
- 写法简单
- 线程安全有保障
- 避免反序列化破坏单例
面试常见问题
- 饿汉式的缺点
- 前期加载资源浪费
- 懒汉式的缺点
- 写法复杂
- 多线程不安全可能
- 为什么要用double-check?不用就不安全吗?
- 为什么双重检查模式要用volatile?
- 创建对象的三个步骤,防止重排序和可见性问题。
- 应该如何选择,用哪种单例的实现方案最好?
- 枚举类
- 实现简单
- 懒汉式
- 线程安全
- 枚举类
五、面试常见问题二
1、讲一讲什么是Java内存模型
起因->和JVM内存模型、Java对象模型的区别->什么是Java内存模型
- 是规范
- 重排序、可见性、原子性
- JMM对主内存和线程内存的抽象
- volatile关键字和synchronized关系
- volatile和synchronized的异同
- synchronize展开,源码,保证可见性,原子性,近朱者赤(可以让附近的代码也可见)
- 哪些操作是原子性的
2、什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作?
3、什么是内存可见性?
cpu各级缓存图。
4、64位的double和long写入的时候是原子的吗
实际操作中并不需要加volatile,商用的JVM已经帮我们实现了