【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());
    }
}

image-20220128111315867

②依赖于前一次的值

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;
    }
}

关键在于这个复制操作和之前的值有没有关系

②刷新之前变量的触发器

image-20220128112902905

image-20220128113135770

4、volatile的两点作用

①可见性

读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存

②禁止指令重排序优化

解决单例双重锁乱序问题

5、volatile和synchronized的关系

volatile可以看做是轻量版的synchronized如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

二、可见性延伸

1、能保证可见性的措施

除了volatile可以让变量保证可见性之外,synchronized、Lock、并发集合、Therad.join()和Thread.start()等都可以保证可见性。

2、对synchronized可见性的正确理解

synchronized不仅保证了原子性,还保证了可见性。

image-20220128114356680

使用上面这样的方式,可以确保synchronized代码块上面的代码也是可见的。

三、原子性

1、什么是原子性

一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。

为什么i++不是原子性的

image-20220128114917721

但是可以用synchronized实现原子性

2、Java中的原子操作有哪些

除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作

所有引用reference的赋值操作,不管是32位的机器还是64位的机器

java.concurrent.Atomic.*包中所有类的原子操作

3、long和double的原子性

官方文档:

对于64位的值的写入,可以分为两个32位的操作进行写入,读取错误、使用volatile解决

image-20220128115457707

结论:在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?

  • 新建对象不是原子操作
  • 新建对象实际上有三个步骤
    • image-20220128122311283

①②③步骤可能发生重排序,产生并发问题。假设线程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() {

    }
}

image-20220128123309161

不同写法对比

  • 饿汉:简单,但是没有lazy loading
    • 如果是对象的创建需要配置文件就不适用
  • 懒汉:有线程安全问题
    • 如果一开始要加载的资源太多,就用懒加载
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:实际开发最好
    • 枚举类反编译发现是是静态的变量,然后还是懒加载
    • image-20220128123554045
    • 写法简单
    • 线程安全有保障
    • 避免反序列化破坏单例

面试常见问题

  • 饿汉式的缺点
    • 前期加载资源浪费
  • 懒汉式的缺点
    • 写法复杂
    • 多线程不安全可能
  • 为什么要用double-check?不用就不安全吗?
  • 为什么双重检查模式要用volatile?
    • 创建对象的三个步骤,防止重排序和可见性问题。
  • 应该如何选择,用哪种单例的实现方案最好?
    • 枚举类
      • 实现简单
      • 懒汉式
      • 线程安全

五、面试常见问题二

1、讲一讲什么是Java内存模型

起因->和JVM内存模型、Java对象模型的区别->什么是Java内存模型

  • 是规范
  • 重排序、可见性、原子性
    • JMM对主内存和线程内存的抽象
    • volatile关键字和synchronized关系
      • volatile和synchronized的异同
      • synchronize展开,源码,保证可见性,原子性,近朱者赤(可以让附近的代码也可见)
    • 哪些操作是原子性的

2、什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作?

3、什么是内存可见性?

cpu各级缓存图。

4、64位的double和long写入的时候是原子的吗

实际操作中并不需要加volatile,商用的JVM已经帮我们实现了

posted @ 2022-01-28 12:49  DarkerG  阅读(52)  评论(0编辑  收藏  举报