JUC并发编程07:单例模式CAS算法和原子引用

Posted 人玉林风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程07:单例模式CAS算法和原子引用相关的知识,希望对你有一定的参考价值。

单例模式

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例

单例模式一般分为两种:饿汉式懒汉式

饿汉式,代码如下:

package com.wunian.juc.single;
/**
* 单例模式:饿汉式
* 单例思想:构造器私有
*/
public class Hungry {

   //浪费空间 不是我们需要的
   private byte[] data=new byte[10*1024*1024];

   private Hungry(){}

   private final static Hungry HUNGRY=new Hungry();

   public static Hungry getInstance(){
       return HUNGRY;
  }
}

懒汉式,基础版,代码如下:

package com.wunian.juc.single;

public class Lazy {

   private Lazy(){
       System.out.println(Thread.currentThread().getName() + " start");
  }

   private static Lazy lazy;

   public static Lazy getInstance() {
       if (lazy == null){
           lazy = new Lazy();
      }
       return lazyMan;
  }

   public static void main(String[] args) {
       // 多线程下单例失效
       for (int i = 0; i < 10; i++) {
           new Thread(()->{
               Lazy.getInstance();
          }).start();
      }
  }
}

DCL(双重校验锁)懒汉式,代码如下:

package com.wunian.juc.single;

public class Lazy {

   private Lazy(){
       System.out.println(Thread.currentThread().getName() + " start");
  }

   private volatile static Lazy lazy;

   public static Lazy getInstance() {
      if (lazy == null){
           synchronized (Lazy.class){
               if (lazy == null){
                   lazy = new Lazy(); // 请你谈谈这个操作!它不是原子性的
                   // java创建一个对象
                   // 1、分配内存空间
                   // 2、执行构造方法,创建对象
                   // 3、将对象指向空间

                   // A 先执行13,这个时候对象还没有完成初始化!
                   // B 发现对象为空,B线程拿到的对象就不是完成的
              }
          }
      }
       return lazy;
  }

   public static void main(String[] args) {
       // 多线程下单例失效
       for (int i = 0; i < 10; i++) {
           new Thread(()->{
               Lazy.getInstance();
          }).start();
      }
  }
}

单例之所以安全,是因为构造器私有的。但是构造器私有也不安全,使用反射就可以绕过构造器直接创建对象,代码如下:

package com.wunian.juc.single;

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

/**
* 单例模式:DCL(Double Check Lock 双重校验锁)懒汉式
*/
public class Lazy {

   private static boolean protectedCode=false;//标记参数,防止被反编译破坏

   private Lazy(){
       synchronized (Lazy.class){
           if(protectedCode==false){
               protectedCode=true;
          }else{
               //病毒代码、文件无限扩容
               throw  new RuntimeException("不要试图破坏我的单例模式");
          }
      }
  }

   private volatile static Lazy lazy;

   public static Lazy getInstance(){
       //双重校验锁
       if(lazy==null){
           synchronized (Lazy.class){
               if(lazy==null) {
                   lazy=new Lazy();//创建对象不是原子性的,还是存在不安全
                   //java创建一个对象
                   //1.分配内存空间
                   //2.执行构造方法,创建对象
                   //3.将对象指向空间
                   //如果A先执行1 3,这个时候对象还没完成初始化!B发现对象为空,B线程拿到的对象就不是完成的
              }
          }
      }
       return lazy;
  }

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

       //反射安全吗?,官方推荐我们单例真的是DCL懒汉式吗?
       //Lazy lazy1=Lazy.getInstance();
       //得到无参构造器
//       Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
//       Lazy lazy2=declaredConstructors.newInstance();//创建对象
//       Lazy lazy3=declaredConstructors.newInstance();//创建对象
//       //hashcode不一样,所以还是不安全 反射根本不需要通过构造器
//       //System.out.println(lazy1.hashCode());
//       System.out.println(lazy2.hashCode());
//       System.out.println(lazy3.hashCode());

       //如何破坏在反编译过程中的保护参数
       Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
       declaredConstructors.setAccessible(true);
       Lazy lazy4=declaredConstructors.newInstance();//创建对象
       //获取参数对象,必须是在知道参数名称的情况下
       Field protectedCode=Lazy.class.getDeclaredField("protectedCode");
       //重新将参数值设置为false
       protectedCode.setAccessible(true);
       protectedCode.set(lazy4,false);
       Lazy lazy5=declaredConstructors.newInstance();//创建对象
       System.out.println(lazy4.hashCode());
       System.out.println(lazy5.hashCode());
  }
}

这个时候就要使用枚举类了。枚举是一个类,实现了枚举的接口,反射无法破坏枚举。代码如下:

package com.wunian.juc.single;

import com.sun.org.apache.bcel.internal.generic.INSTANCEOF;

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

/**
* 单例模式:使用枚举类
* 枚举是一个类,实现了枚举的接口
* 反射 无法破坏枚举
*/
public enum SingleEnum {

   INSTANCE;

   public SingleEnum getInstance(){
       return INSTANCE;
  }
}
//至少在做一个普通的jvm的时候,jdk源码没有被修改的时候,枚举就是安全的
//可以通过修改 jdk/jre/lib/rt.jar中java.lang.reflect.Constructor.class来破坏枚举(见Constructor类)
class Demo{
   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
       //通过jad.exe反编译可知,SingleEnum类只有一个有参构造器
       //Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
       Constructor<SingleEnum> declaredConstructor=SingleEnum.class.getDeclaredConstructor(String.class,int.class);
       declaredConstructor.setAccessible(true);
       // throw new IllegalArgumentException("Cannot reflectively create enum objects");
       SingleEnum singleEnum1=declaredConstructor.newInstance();
       SingleEnum singleEnum2=declaredConstructor.newInstance();
       System.out.println(singleEnum1.hashCode());
       System.out.println(singleEnum2.hashCode());
       //这里面没有无参构造!JVM才是王道
       //java.lang.NoSuchMethodException: com.wunian.juc.single.SingleEnum.<init>()
  }
}

运行代码会发现,报了一个异常:Cannot reflectively create enum objects,因此无法通过反射破坏枚举。但是如果修改jdk源码,枚举也可能变得不安全,但至少一般情况下枚举还是安全的。

CAS算法

CAS,CompareAndSwap比较并交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

示例代码如下:

package com.wunian.juc.jmm;

import java.util.concurrent.atomic.AtomicInteger;

/**
* CAS:比较并交换
*/
public class CompareAndSwapDemo {

   public static void main(String[] args) {
       //AtomicInteger默认为0
       AtomicInteger atomicInteger=new AtomicInteger(5);
       //compareAndSwap CAS 比较并交换
       //如果这个值是期望的值,则更新为指定的值,交换成功返回true,否则返回false
       System.out.println(atomicInteger.compareAndSet(5,20));
       System.out.println(atomicInteger.get());//输出当前的值
       System.out.println(atomicInteger.compareAndSet(20, 5));
  }
}

分析AtomicInteger类的getAndIncrement方法

getAndIncrement方法实现了int++的原子性操作,它底层是如何实现的呢?来看看它的源码:

// unsafe可以直接操作内存
public final int getAndIncrement() {
   // this 调用的对象
   // valueOffset 当前这个对象的值的内存地址偏移值
   // 1
   return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5; // ?
   do { // 自旋锁(就是一直判断!)
       // var5 = 获得当前对象的内存地址中的值!
       var5 = this.getIntVolatile(this, valueOffset); // 1000万
       // compareAndSwapInt 比较并交换
       // 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
       // 如果是期望的值,就交换,否则就不交换!
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

   return var5;
}

可以看出,getAndIncrement的底层是通过自旋锁CAS算法来实现的。

CAS的缺点

  • 循环开销很大。

  • 内存操作,每次只能保证一个共享变量的原子性。

  • 可能出现ABA问题。

原子引用

什么是ABA问题?

简单来说就是狸猫换太子,例如有两个线程T1和T2,T1线程希望通过CAS算法将一个变量的值由100更新为1,结果在更新过程中睡眠了3秒,在这三秒中T2线程也通过CAS算法先将该变量值更新由100更新为1,然后又将该变量值由1再次更新为100,整个过程看起来似乎该变量的值没有改变,但是对于T1线程来说,数据已经改动了。

如何解决ABA问题?

可以使用原子类,通过增加一个版本号来解决,原理和乐观锁一样。例如,小明和小花同时更新一个数据,小明先睡了三秒,结果数据先被小花更新了,这时版本号发生变化,小明再去更新就会失败,代码如下:

package com.wunian.juc.jmm;

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

/**
* ABA问题 1-100-1
* 通过version字段加1来实现数据的原子性
*/
public class ABADemo {

   //version =1
   static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);

   public static void main(String[] args) {

       //其他人员 小花 需要每次执行完毕+1
       new Thread(()->{
           int stamp=atomicStampedReference.getStamp();//获得版本号

           System.out.println("T1 stamp01=>"+stamp);

           try {
               TimeUnit.SECONDS.sleep(1);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           atomicStampedReference.compareAndSet(100,101,
                   atomicStampedReference.getStamp(),
                   atomicStampedReference.getStamp()+1);

           System.out.println("T1 stamp02=>"+atomicStampedReference.getStamp());

           atomicStampedReference.compareAndSet(101,100,
                   atomicStampedReference.getStamp(),
                   atomicStampedReference.getStamp()+1);
           System.out.println("T1 stamp03=>"+atomicStampedReference.getStamp());

      },"T1").start();

       //乐观的小明,sleep过程中数据被小花改过,版本号发生变化,无法完成更新
       new Thread(()->{
           int stamp=atomicStampedReference.getStamp();//获得版本号
           System.out.println("T2 stamp01=>"+stamp);
           try {
               TimeUnit.SECONDS.sleep(3);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           boolean result=atomicStampedReference.compareAndSet(100,1,stamp,stamp+1);
           System.out.println("T2是否修改成功:"+result);
           System.out.println("T2 stamp02=>"+atomicStampedReference.getStamp());
           System.out.println("T2 当前获取得最新的值=>"+atomicStampedReference.getReference());
      },"T2").start();
  }
}



推荐阅读



扫码关注“人玉林风”,获取更多精彩内容。

以上是关于JUC并发编程07:单例模式CAS算法和原子引用的主要内容,如果未能解决你的问题,请参考以下文章

深入单例模式和深入理解CAS模式

java基础---多线程---JUC原子类

聊聊并发——CAS算法

Java Review - 并发编程_原子操作类原理剖析

juc学习二(CAS底层原理)

重点知识学习(8.2)--[JMM(Java内存模型),并发编程的可见性原子性有序性,volatile 关键字,保持原子性,CAS思想]