Java并发编程6_单例模式、理解CAS、原子引用解决ABA问题

目录

一、单例模式

二、理解CAS

三、原子引用解决ABA问题


一、单例模式

什么是单例模式:一个类只有一个实例

单例模式的几种实现方式:

  • 饿汉式
  • 一般懒汉式,线程不安全
  • 加锁懒汉式,线程安全
  • DCL双重校验锁
  • 静态内部类
  • 枚举

 1.饿汉式

  特点

  • 构造方法私有化
  • 定义成员变量new一个实例作为初始值(饿)
  • 提供获取实例的静态方法
  • 优点:没有加锁,执行效率会提高
  • 缺点:类加载时就初始化,浪费内存资源

 举例:

/**
 * 饿汉式
 */
public class HungryMan {
    
    
    // 因为是getInstance是静态方法,因此程序开始,HungryMan对象就被加载进内存,因此内存中就存在了下面data1-data4的数据,导致浪费内存空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];



    // 私有构造方法
    private HungryMan(){

    }

    private static final HungryMan HUNGRY_MAN = new HungryMan();

    public static HungryMan getInstance(){
        return HUNGRY_MAN;
    }
}

 2.一般懒汉式

特点

  • 需要使用单例时,单例才初始化占用内存
  • 单线程下没问题,但是多线程下是不安全的,会出现创建多个不同实例
  • 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程,因为没有加锁 synchronized,所以严格意义上它并不算单例模式

举例:

/**
 * 一般懒汉式
 * 单线程下没问题,但是多线程有可能出现问题
 */
public class LazyMan {



    private static LazyMan lazyMan;

    private LazyMan(){

        System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
        /*输出 发现多线程下懒汉式 不能保证一个单例

        1 线程拿到了实例!
        2 线程拿到了实例!
        0 线程拿到了实例!

         */
    }

    public static LazyMan getInstance(){
        // 需要使用单例的情况下 且 实例还没有被创建 才创建单例,
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }


    public static void main(String[] args) {
        // 模拟是个线程获取实例
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();

            },String.valueOf(i)).start();
        }

    }

}

3.加锁懒汉式

 特点 

  • 相比一般的懒汉式,在获取实例的静态方法前加synchronized关键字,可以保证多线程之间的同步问题,保证了单例模式
  • 优点:第一次调用才初始化,避免内存浪费
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率 

举例:  

/**
 * 加synchronized懒汉式
 */
public class LazyManSynchronizedMethod {



    private static LazyManSynchronizedMethod lazyMan;

    private LazyManSynchronizedMethod(){

        System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
        /*输出 发现多线程下懒汉式 不能保证一个单例

        0 线程拿到了实例!

         */
    }
    
    // 获取单例的方法加上synchronized锁,保证了单例的线程安全 
    public static synchronized LazyManSynchronizedMethod getInstance(){
        // 需要使用单例的情况下 且 实例还没有被创建 才创建单例,
        if(lazyMan == null){
            lazyMan = new LazyManSynchronizedMethod();
        }
        return lazyMan;
    }


    public static void main(String[] args) {
        // 模拟是个线程获取实例
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyManSynchronizedMethod.getInstance();

            },String.valueOf(i)).start();
        }

    }

}

4.DCL双重校验锁

特点 

  • DCL即 double-checked locking
  • 相比一般懒汉式,通过if双重判断 + synchronized 同步代码块 来解决保证线程之间的同步问题
  • 相比加锁的懒汉式,不是在方法前面加synchronized从而影响效率,这种方法效率更高
  • 这种方式采用双锁机制,安全且在多线程情况下能保持高性能
/**
 * DCL双重锁 懒汉式
 */
public class LazyManDCL {


    private static LazyManDCL lazyMan;

    private LazyManDCL(){

        System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
        /*输出 : 0 线程拿到了实例!
         发现多线程下DCL懒汉式 能保证一个单例
         */
    }

    public static LazyManDCL getInstance(){
        // DCL 双重校验判断

        // 第一次判断,没有实例的时候给类加锁
        if(lazyMan == null){
            synchronized (LazyManDCL.class){

                // 第二次判断,没有实例的时候,创建实例
                if(lazyMan == null){
                    lazyMan = new LazyManDCL();
                }
            }
        }

        return lazyMan;
    }


    public static void main(String[] args) {
        // 模拟10个线程获取实例
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyManDCL.getInstance();

            },String.valueOf(i)).start();
        }

    }

}

问题:

  • 这样做还是不够的,因为 lazyMan = new LazyManDCL(); 在JMM模型中并不是原子性操作,创建实例大致分为了三步:
    1. 堆中分配内存空间
    2. 执行构造方法,初始化实例
    3. 将实例变量指向内存空间
    • 其中最后 两步 的顺序是可以变得,也就是可以按照 1 2 3 或者 1 3 2 的顺序执行,但是在多个线程共同执行时,有可能就会出现问题:
      • 第一个线程1执行了 1 3 剩下 2 未执行,因为3 执行了地址已经存在 if(实例 == null )就不再成立,但是此时实例还没构造完成
      • 另一个线程2执行到 if(实例 == null) 发现不成立,就会认为对象已经创建成功,直接返回实例,但是得到的实例因为还没有初始化,得到的就是一个null

解决方案:

  • 使用volatile关键字禁止指令重排 解决 创建实例非原子性 的问题
  • 这才是一个完整的懒汉式单例模式保证线程安全的方案

举例:

/**
 * DCL双重锁 懒汉式
 */
public class LazyManDCL {

    // 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
    private static volatile LazyManDCL lazyMan;

    private LazyManDCL(){

        System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
        /*输出 : 0 线程拿到了实例!
         发现多线程下DCL懒汉式 能保证一个单例


         */
    }

    public static LazyManDCL getInstance(){
        // DCL 双重校验判断

        // 第一次判断,没有实例的时候给类加锁
        if(lazyMan == null){
            synchronized (LazyManDCL.class){

                // 第二次判断,没有实例的时候 获得单例
                if(lazyMan == null){
                    lazyMan = new LazyManDCL();
                }
            }
        }

        return lazyMan;
    }


    public static void main(String[] args) {
        // 模拟10个线程获取实例
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyManDCL.getInstance();

            },String.valueOf(i)).start();
        }

    }

}

5.反射机制破解单例模式(枚举除外)

概念

  • 单例模式要求构造方法私有化,达到只有一个实例的目的,但是通过反射可以忽略构造方法的私有化,得到实例,破坏单例模式
  • 基本步骤
    • 反射获得无参构造器对象
    • 无参构造器对象设置忽略构造方法私有化
    • 无参构造对象.newInstance()得到实例

举例:

package cn.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;


public class LazyManDCLReflection {

    // 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
    private static  volatile LazyManDCLReflection lazyMan;

    private LazyManDCLReflection(){

        System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
        /*输出 : 0 线程拿到了实例!
         发现多线程下DCL懒汉式 能保证一个单例
         */
    }

    // 获取单例
    public static LazyManDCLReflection getInstance(){
        // DCL 双重校验判断

        // 第一次判断,没有实例的时候给类加锁
        if(lazyMan == null){
            synchronized (LazyManDCLReflection.class){

                // 第二次判断,没有实例的时候 获得单例
                if(lazyMan == null){
                    lazyMan = new LazyManDCLReflection();
                }
            }
        }
        return lazyMan;
    }


    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {


        // 1.单例模式获取
        LazyManDCLReflection instance = LazyManDCLReflection.getInstance();
        System.out.println(instance);

        // 2.反射获取

        // 获取LazyManDCLReflection类的无参构造器
        Constructor<LazyManDCLReflection> declaredConstructor = LazyManDCLReflection.class.getDeclaredConstructor(null);
        // 忽略构造器私有设置
        declaredConstructor.setAccessible(true);
        // 创建实例
        LazyManDCLReflection instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
    }
}

输出:

main 线程拿到了实例!
cn.test.LazyManDCLReflection@1540e19d
main 线程拿到了实例!
cn.test.LazyManDCLReflection@677327b6

5.1 解决反射绕过单例模式问题 -- 红绿灯标志

思路:

  • 既然反射是通过调用构造器去创建实例的,那么就在私有构造方法中增加一个判断:
    • 先定义一个成员变量作为标志:private static Boolean flag = false
    • 创建单例调用构造方法时,在构造方法中就将flag = true
    • 当第二次通过反射去调用构造器创建对象的时候,因为flag 值改变,就抛出异常处理
  • 但是这样仍然存在问题,即虽然 标志位flag 是私有的,还是可以通过反射获取到flag,将标志位flag重新修改为true,又可以重新破坏单例模式

举例:

package cn.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;


public class LazyManDCLReflection {

    // 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
    private static  volatile LazyManDCLReflection lazyMan;
    // 增加一个标志位
    private static boolean flag = true;

    private LazyManDCLReflection(){

        if (flag == true) {
            flag = false;
        }else {// 进入此代码块,说明有人在使用反射破环单例模式,那么抛出异常
            throw new RuntimeException("请勿使用反射破环单例模式!");
        }
    }

    // 获取单例
    public static LazyManDCLReflection getInstance(){
        // DCL 双重校验判断

        // 第一次判断,没有实例的时候给类加锁
        if(lazyMan == null){
            synchronized (LazyManDCLReflection.class){

                // 第二次判断,没有实例的时候 获得单例
                if(lazyMan == null){
                    lazyMan = new LazyManDCLReflection();
                }
            }
        }
        return lazyMan;
    }


    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {


        // 1.单例模式获取
        LazyManDCLReflection instance = LazyManDCLReflection.getInstance();
        System.out.println(instance);

        // 2.反射获取

        // 获取LazyManDCLReflection类的无参构造器
        Constructor<LazyManDCLReflection> declaredConstructor = LazyManDCLReflection.class.getDeclaredConstructor(null);
        // 忽略构造器私有设置
        declaredConstructor.setAccessible(true);
        // 创建实例
        LazyManDCLReflection instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
    }
}

输出:
cn.test.LazyManDCLReflection@1540e19d // 第一次正常通过getInstance获取单例
Exception in thread "main" java.lang.reflect.InvocationTargetException // 第二次使用反射获取
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at cn.test.LazyManDCLReflection.main(LazyManDCLReflection.java:55)
Caused by: java.lang.RuntimeException: 请勿使用反射破环单例模式!
	at cn.test.LazyManDCLReflection.<init>(LazyManDCLReflection.java:19)
	... 5 more

5.2 使用枚举防止反射破环单例模式

关于枚举:

  • 使用枚举测试反射破坏单例模式时候,发现报的错不是 Cannot reflectively create enum objects,而是找不到指定构造方法
  • 枚举的构造参数虽然在IDE集成环境看到的枚举类的构造器是无参构造,而且通过进入字节码文件的当前目录,cmd进入控制台,输入命令:javap -p Xxx.class 反编译字节码文件查看,枚举类也是只有一个无参构造,但其实是枚举的构造方法是 有两个 参数的构造方法
  • enum也是一个特殊的类,可以通过反编译字节码查看 javap -p Xxx.class
D:\software\IDEA\idea_workspace\test001\out\production\test001\cn\test>javap -p EnumSingle
警告: 二进制文件EnumSingle包含cn.test.EnumSingle
Compiled from "EnumSingle.java"
public final class cn.test.EnumSingle extends java.lang.Enum<cn.test.EnumSingle> {// 枚举类也是一个继承Enum的类========================
  public static final cn.test.EnumSingle INSTANCE;
  private static final cn.test.EnumSingle[] $VALUES;
  public static cn.test.EnumSingle[] values();
  public static cn.test.EnumSingle valueOf(java.lang.String);
  private cn.test.EnumSingle(); // 无参构造=====================================
  private static cn.test.EnumSingle getInstance();
  static {};
}
  • 打开jad生成的反编译文件,才发现枚举类并不是空参构造 ,而是private EnumSingle(String s, int i) {super(s, i);}

测试反射破坏枚举单例模式:

package cn.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingle {
    // 定义一个实例
    INSTANCE;

    // 私有化构造器
    private EnumSingle() {}

    // 获取实例
    private static EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 测试反射破坏枚举单例模式
        // 反射获取到构造器
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        // 设置忽略构造器私有化
        constructor.setAccessible(true);
        // 获取实例
        EnumSingle enumSingle = constructor.newInstance();
        System.out.println(enumSingle);

    }

}

输出:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at cn.test.EnumSingle.main(EnumSingle.java:25)

结果分析:

  • 显然 反射不能破坏 枚举的单例模式 抛了异常:java.lang.IllegalArgumentException: Cannot reflectively create enum objects

二、理解CAS

概念:

  • CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

举例:

import java.util.concurrent.atomic.AtomicInteger;


/*
简单cas举栗
 */
public class Demo1 {

    public static void main(String[] args) {

        AtomicInteger num = new AtomicInteger(100);
        // 参数为 (期望值 ,更新后的值),当100与AtomicInteger的默认值相同时,就会将100更新为200
        num.compareAndSet(100,200);
        System.out.println(num.compareAndSet(200, 300)); // true 更新成功
        System.out.println(num.compareAndSet(200, 300)); // false 更新失败
        System.out.println(num.get());
    }
}

关于Unsafe类

  • java不能直接操作地址,需要调用native方法即调用c++操作内存,Unsafe类里面都是native方法
  • AtomicsInteger的getAndIncrement()方法底层就是调用的Unsafe类的getAndAddInt()方法

 源码:

	======= AtomicsInteger 的 cas 方法 =======
	/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
	============== Unsafe 源码 ==========
	public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 
        return var5;
    }

源码理解:

在这里插入图片描述

  • 以上源码的:

  • do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

  • var1:当前对象   var2:当前对象的内存值  var5:从当前对象取出来的内存值

  • this.compareAndSwapInt(var1, var2, var5, var5 + var4 的意思是:取出当前对象var1中的内存值var2与var5作比较,如果相同,就将var5 = var5+var4(这就是一个新值),最后将var5返回

  • 本质是自旋锁 -- 判断内存位置的值是否是当前期望的值,是就更新,不是就一直自旋等待

 CAS缺点:

  • 自旋锁循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • 会存在ABA问题

三、原子引用解决ABA问题

1.举栗理解ABA问题

  • 正常线程1、2之间穿插了一个捣乱线程,但是捣乱线程的操作了数据,又将数据数据恢复到1线程数据修改完成后的状态,2线程正常执行任务,它根本不知道捣乱线程的有修改过数据

代码举例:

package cn.test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


public class LazyManDCLReflection {

    private static AtomicInteger num = new AtomicInteger(1000);

    public static void main(String[] args) {


        new Thread(()->{
            num.compareAndSet(1000,2000);
            // 获取AtomicInteger的值2000
            System.out.println(num.get());

        },"正常线程1").start();


        // 休眠2s
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 捣乱线程虽然不影响最终的结果,但是正常流程之间加了一个其他操作
        new Thread(()->{
            num.compareAndSet(2000,1000);
            System.out.println(num.get());

            num.compareAndSet(1000,2000);
            System.out.println(num.get());

        },"捣乱线程").start();


        // 休眠2s
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            num.compareAndSet(2000,3000);
            System.out.println(num.get());


        },"正常线程2").start();

    }
}

2.ABA的危害

  • 一般场景下ABA并不会出现什么问题,但是当涉及到中间过程的时候就会出问题
  • 场景:有一天,老铁到ATM机去取款,使用ATM查询之后,老铁发现它银行卡的余额还有200,于是老铁想去100块给女朋友买小礼物,但是老铁取款时,在点击取款按钮后机器卡了一下,滑稽老铁下意识又点了一下,假设这两部取款操作执行图如下:

  • 如果没有出现意外,即使按下两次取款按钮也是正常的,但是在这两次CAS操作之间,如图老铁的朋友给它转账了100块,导致第一次CAS扣款100后的余额从100变回到了200,这时第二次CAS操作也会执行成功,导致又被扣款100块,最终余额是100块,这种情况是不合理的,合理的情况应该是第二次CAS仍然失败,最终余额为200元。

 3.原子引用解决ABA问题

  • 简单理解就是带版本号的原子操作,为了避免ABA出现,因此在每一次数据被修改以后,都会更新一个版本号或者时间戳
  • AtomicReference<V> 类可以提供原子引用操作,时间戳可以作为版本号标记
  • 另外需要注意AtomicReference<Integer> 因为涉及Integer 127到-128的整数常量池,可能导致CAS操作失败
package cn.test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;


public class LazyManDCLReflection {

    // AtomicStampedReference第一个参数是内存值,第二个是版本号
    private static AtomicStampedReference<Integer> num = new AtomicStampedReference(1, 1);

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            // 打印版本号
            System.out.println("A线程版本号:"+ num.getStamp());
            // 将num的值改为2,compareAndSet方法:第一个参数是预期值,第二个参数是要修改为的值,死三个参数是版本号,第四个参数是更新版本号
            // 操作成功的条件:
            // 1. 核对版本号是否与上一次修改后的版本号一致
            // 2. 1与num的内存值相等
            // 3. 将1改为2
            System.out.println(num.compareAndSet(1, 2, num.getStamp(), num.getStamp() + 1));
        }, "A").start();

        // 线程睡眠2S, 保证A线程执行完成
        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            // 打印版本号
            System.out.println("捣乱线程版本号1:"+ num.getStamp());
            // 将num的值改为3
            System.out.println(num.compareAndSet(2, 3, num.getStamp(), num.getStamp() + 1));
            // 打印版本号
            System.out.println("捣乱线程版本号2:"+ num.getStamp());
            // 将num的值改回来
            System.out.println(num.compareAndSet(3, 2, num.getStamp(), num.getStamp() + 1));
        }, "捣乱线程").start();

        // 线程睡眠2S, 保证捣乱线程执行完成
        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            // 打印版本号
            System.out.println("B线程版本号:"+ num.getStamp());
            // 将num的值改为66
            System.out.println(num.compareAndSet(2, 66, 1, num.getStamp() + 1));
        }, "B").start();
    }
}

输出:
A线程版本号:1
true
捣乱线程版本号1:2
true
捣乱线程版本号2:3
true
B线程版本号:4
false


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值