并发编程(JUC)系列(5)--CAS&Atomic系列

187 阅读32分钟

写在前面:

文章内容是通过个人整理以及参考相关资料总结而出,难免会出现部分错误

如果出现错误,烦请在评论中指出!谢谢


1 CAS

我们平时熟悉的有锁并发的典型代表就是synchronize,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其原理时通过当前线程持有当前对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,从而保证线程安全

但是我们马上就要了解另外一种反向而行的并发策略,即无锁并发,在不加锁的情况下也能保证程序并发的安全性

1.1 无锁

在谈及无锁概念时,总会关联起乐观派和悲观派,对于乐观派而言,他们认为事情总会向好的方向发展,总是认为坏的情况发生的概率特别低,可以无所顾忌的做事;但是对于悲观派而言,他们总会认为发展时态如果不及时控制,以后就无法挽回,即使无法挽回的情况几乎不可能发生

这两种派系映射到并发编程中就如同加锁和无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略。因为对于加锁的并发程序来说,他们总是认为每次访问共享资源时总会发生冲突,因此必须对每次数据操作实施加锁策略;而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发生冲突,无锁策略则采用一种CAS技术来保证线程执行的安全性

1.2 无锁的执行者-CAS

1.2.1 CAS

CAS的全称时Compare And Swap即比较交换,其算法核心思想为:

执行函数:CAS(V,E,N)

这其中的3个参数:

  • V表示要更新的变量
  • E表示预期值
  • N表示新值

如果V值等于E值,则将V的值设为N,若V值和E值不同,说明已经有其他线程做了更新,则当前线程就不进行改变

通俗理解CAS操作需要我们提供一个期望值,当期望值和当前线程的变量值相同时,说明还没线程操作该值,当前线程可以进行修改,也就可以执行CAS操作;如果期望值和当前线程的变量值不同,说明该值已经被其他线程修改,此时就不执行更新操作,但可以选择重新读取该变量并再次尝试修改该变量,也可以放弃操作,原理图如下:

image-20210225211217373

由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出

基于这种原理,CAS操作即使没有锁,同样知道其他线程对共享资源的操作影响,并执行相应的处理措施,同时这点也可以看出来,由于无锁操作中没有锁的存在,就不可能出现死锁的情况,也就是CAS天生免疫死锁

1.2.2 CPU指令对CAS的支持

或许我们有这样的疑问,假设存在多个线程执行CAS操作并且CAS步骤很多,有没有可能在判断V和E相同后正要赋值时,切换到了其他线程,对值进行了修改,导致数据不一致呢?

答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语,是由若干条指令组合而成,用语完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断

也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

1.3 Unsafe类(指针)

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就知道该类是不安全的,毕竟Unsafe类拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类

Java官方也不建议直接使用Unsafe类,但是我们依旧有必要了解该类,因为CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类的所有方法都是native修饰,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相关任务,关于Unsafe类的主要功能点如下:

内存管理,Unsafe类中存在直接操作内存的方法

//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);

//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte  getByte(long address);
public native void  putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同

//操作系统的内存页大小
public native int pageSize();

提供实例对象新途径

//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;

类和实例对象以及变量的操作,主要方法如下

//获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);


//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
//通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
//设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);

//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同

//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void  putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);

//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile及getIntVolatile相同,引用类型putObjectVolatile也一样。

//与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o,long offset,int x);

对数组进行操作

//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);

线程挂起和恢复操作

将一个线程进行挂起是通过park方法实现,调用park后线程会一直阻塞知道超时或中断等条件出现,unpark可以终止一个挂起的线程,使其恢复正常;Java对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现还是使用Unsafe.park()方法和Unsafe.unpark()方法

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
public native void unpark(Object thread); 

那么下面可以引入下操作系统的挂起态

挂起态

//TODO

1.3.1 测试Unsafe类

public class UnsafeDemo {

    public static void main(String[] args) throws Exception {
        // 通过反射得到Unsafe类theUnsafe属性对应的Field对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置该Field为可访问
        field.setAccessible(true);
        // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe.toString());

        //通过allocateInstance直接创建对象
        User user = (User) unsafe.allocateInstance(User.class);

        Class userClass = user.getClass();
        Field name = userClass.getDeclaredField("name");
        Field age = userClass.getDeclaredField("age");
        Field id = userClass.getDeclaredField("id");

        //获取实例变量name和age在对象内存中的偏移量并设置值
        unsafe.putInt(user,unsafe.objectFieldOffset(age),18);
        unsafe.putObject(user,unsafe.objectFieldOffset(name),"hello jiang");

        // 这里返回User.class
        Object staticBase = unsafe.staticFieldBase(id);
        System.out.println("staticBase:" + staticBase);

        //获取静态变量id的偏移量staticOffset
        long staticOffset = unsafe.staticFieldOffset(id);
        // 获取静态变量的值
        System.out.println("设置前的ID:" + unsafe.getObject(staticBase,staticOffset));
        // 设置值
        unsafe.putObject(staticBase,staticOffset,"jiang xiaopang");
        //获取静态变量的值
        System.out.println("设置后的ID:" + unsafe.getObject(staticBase,staticOffset));

        long data = 1000;
        byte size = 1;

        //调用allocateMemory分配内存,并获取内存地址memoryAddress
        long memoryAddress = unsafe.allocateMemory(size);
        //直接往内存写入数据
        unsafe.putAddress(memoryAddress,data);
        //获取指定内存地址的数据
        long addrData = unsafe.getAddress(memoryAddress);
        System.out.println("addData:" + addrData);
    }
}


@ToString
class User {
    private String name;
    private int age;
    private static String id="USER_ID";

    public User(){
        System.out.println("user 构造方法被调用");
    }
}

输出结果

sun.misc.Unsafe@14ae5a5
staticBase:class org.jiang.testUnsafe.User
设置前的ID:USER_ID
设置后的ID:jiang xiaopang
addData:1000

虽然在Unsafe类中存在getUnsafe()方法,但该方法只提供给高级的Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在Demo中使用了反射技术获取了Unsafe实例对象并进行相关操作(其实就是获取Unsafe类中的实例对象)

public static Unsafe getUnsafe() {
      Class cc = sun.reflect.Reflection.getCallerClass(2);
      if (cc.getClassLoader() != null)
          throw new SecurityException("Unsafe");
      return theUnsafe;
  }

1.3.2 Unsafe针对CAS相关操作

CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现(稍后讲解的Atomic系列内部方法也是基于下面的方法实现)

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

这里还需要介绍Unsafe类中Java8新增的几个方法,他们的实现都是基于上述的CAS方法,如下:

 //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

//1.8新增,方法作用同上,只不过这里操作的long类型数据
 public final long getAndAddLong(Object o, long offset, long delta) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, v + delta));
     return v;
 }

 //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndSetInt(Object o, long offset, int newValue) {
     int v;
     do {
         v = getIntVolatile(o, offset);
     } while (!compareAndSwapInt(o, offset, v, newValue));
     return v;
 }

// 1.8新增,同上,操作的是long类型
 public final long getAndSetLong(Object o, long offset, long newValue) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, newValue));
     return v;
 }

 //1.8新增,同上,操作的是引用类型数据
 public final Object getAndSetObject(Object o, long offset, Object newValue) {
     Object v;
     do {
         v = getObjectVolatile(o, offset);
     } while (!compareAndSwapObject(o, offset, v, newValue));
     return v;
 }

上述的方法我们在Atomic系列分析中依然会见到它们的身影

1.3.3 基于Unsafe#getAndAddInt分析

在上面我们已经介绍了Unsafe#getAndAddInt方法的源码

public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

这里实际上就是先创建一个局部变量v,然后通过getIntVolatile()方法获取对象o偏移量为offset的属性值,然后调用compareAndSwapInt()方法不断进行比较交换

我们知道compareAndSwapInt()方法就是先判断对象o偏移量为offset的属性值是否依然为v,如果是v就交换为v + delta,并且返回true,反之依然

假如这里返回false则继续进入循环,将变量v修改为最新的对象o偏移量为offset的属性值然后再进行判断

那么只要有其他线程进行了修改,那么循环就持续执行并获取最新的属性的值,直到没有其他线程进行修改为止

1.4 Atomic系列(原子操作)

通过前面的分析我们已经了解了无锁CAS的原理并对Java中的指针类Unsafe类有了相对的认识,下面进一步分析CAS在Java中的应用,即并发包中的院子操作类(Atomic系列),从JDK1.5开始提供java.util.concurrent.atomic包,在该包中提供了很多基于CAS实现的原子操作类,主要分为4种类型

1.4.1 原子更新基本类型

原子更新基本类型主要包括3个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

这3个类的实现原理和使用方式几乎一样,这里就以AtomicInteger为例进行分析,AtomicInteger主要针对int类型的数据执行原子操作,它提供了原子自增、原子自减以及原子赋值等方法,下面首先来看下源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 获取指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //下述变量value在AtomicInteger实例对象内的内存偏移量
    private static final long valueOffset;

    static {
        try {
           //通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
           //通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
   //当前AtomicInteger封装的int变量value
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    }
   //获取当前最新值,
    public final int get() {
        return value;
    }
    //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。
    public final void set(int newValue) {
        value = newValue;
    }
    //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
   //设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
   //如果当前值为expect,则设置为update(当前值指的是value变量)
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //当前值加1返回旧值,底层CAS操作
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //当前值减1,返回旧值,底层CAS操作
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
   //当前值增加delta,返回旧值,底层CAS操作
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //当前值加1,返回新值,底层CAS操作
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //当前值减1,返回新值,底层CAS操作
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
   //当前值增加delta,返回新值,底层CAS操作
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
   //省略一些不常用的方法....
}

那么首先来分析最开始AtomicInteger都做了什么?

1、获取指针类Unsafe中的实例对象用于指针操作

2、我们知道AtomicInteger中的value属性实际上就是创建AtomicInteger对象时传入的初始值参数,也就是封装的int值

3、通过初始化过程中的静态代码块获取value属性在AtomicInteger对象中的内存偏移地址

接下来看下主要进行操作的方法都是怎么实现的呢?

对于getAndSet()、compareAndSet()、getAndAdd()这些方法实际上都是调用的Unsafe类中的CAS相关操作的方法,所以说明AtomicInteger是基于无锁实现的

那么就拿JDK1.8新增的getAndAddInt()方法进行举例,源码如下

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
       v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

可以看出这是一个do...while循环,循环的判断条件是!compareAndSwapInt(o, offset, v, v + delta),在Unsafe类中我们已经知道通过变量o和变量offset我们可以获得指定偏移定义的属性值,而变量v就是我们第一次从主内存中获取到的值,然后我们希望把这个值改为v + delta

这时如果主内存的值已经发生了改变,那么CAS操作返回的布尔值就为false,从而变量v就需要重新获取指定偏移量定义的属性值,然后继续循环

只有当主内存的值并没有发生改变,CAS返回的布尔值为true,这时才会跳出循环并成功修改主内存的值

这样我们就保证这次修改的操作是基于无锁的原子操作(在不考虑ABA的前提下)

12.4.2 AtomicInteger方法原理分析

在上面我们已经熟悉了Unsafe类的使用方法,并且了解AtomicInteger和AtomicBoolean原理基本相同,那么下面就以AtomicInteger#getAndSet为例来分析其实现原理

下面我们先对AtomicInteger构造方法进行分析,我们知道当调用构造器时会进行类的初始化,执行一些静态变量和静态代码块的执行(也就是cinit方法)

AtomicInteger类内部维护了一个int类型的value变量(这里需要注意value变量用volatile修饰,就是为了确保变量的可见性),静态代码块中会通过:

valueOffset = unsafe.objectFieldOffset
    (AtomicInteger.class.getDeclaredField("value"));

获取value变量的内存偏移地址,并赋值给valueOffset变量

并且类内部维护了一个静态变量unsafe:

private static final Unsafe unsafe = Unsafe.getUnsafe()

我们知道Unsafe类中不能直接通过构造器创建对象,因此这里就是获取Unsafe类的一个实例

然后构造器中将参数赋值给value变量


经过初始化之后下面就开始分析getAndSet方法

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

这里可以看出来实际上就是调用Unsafe类的getAndSetInt()方法,而在上面我们已经分析过Unsafe类的一些方法,那么原理就基本一致

image-20210307160117420

实际上也是利用CAS的特性进行循环并不断取出主存中的最新值,然后和预期值进行比较并判断是否可以更新

1.4.3 原子更新引用

原子更新引用类型可以同步更新引用类型,我们知道对于AtomicInteger、AtomicBoolean等原子类实际上就是对基本数据类型进行原子更新,那么如果遇到自定义类型应该怎么办呢?这里就需要使用到原子引用类型AtomicReference,而其基本原理相似

这里简单举例子进行说明:


定义自定义类用于操作:

@Data
@AllArgsConstructor
@NoArgsConstructor
class Person {
    private String name;
    private int age;
}

定义测试类进行测试:

@Slf4j
public class Demo7 {
    private static AtomicReference<Person> reference = new AtomicReference<>();

    public static void main(String[] args) {
        Person person = new Person("jiang", 18);
        reference.set(person);
        Person newPerson = new Person("nidi", 23);
        person.setName("liu");
        boolean flag = reference.compareAndSet(person, newPerson);
        log.info(String.valueOf(flag));
        log.info(reference.get().toString());
    }
}

这里AtomicReference类上的泛型就是引用类型,传入不同的泛型实际上就是对不同类型的对象进行原子操作

reference.set(person)实际上就是类似于AtomicInteger类中通过构造器对value变量进行赋值


测试结果

image-20210307162036211

由于数据没有其他线程进行修改,因此期望值和主存中变量的值一致,故修改成功

修改期望值进行测试

@Slf4j
public class Demo7 {
    private static AtomicReference<Person> reference = new AtomicReference<>();

    public static void main(String[] args) {
        Person person = new Person("jiang", 18);
        reference.set(person);
        Person newPerson = new Person("nidi", 23);
        Person updatePerson = new Person("liu", 18);
        boolean flag = reference.compareAndSet(updatePerson, newPerson);
        log.info(String.valueOf(flag));
        log.info(reference.get().toString());
    }
}

测试结果

image-20210307162258833

这里实际上就是模拟存在其他线程对变量进行了修改,可以看到原子修改失败

1.4.2.1 ABA问题

我们知道CAS实际上就是比较并交换,那么这里思考一个问题:

如果某个线程在获取到最新变量值之后且比较并交换之前,其他线程对变量进行了两次操作,一次修改为其他值,另一次又修改回来,那么这时CAS在比较并交换时发现变量值和期望值相同,难道这就意味着并没有其他线程进行操作嘛?,上面的问题可以用下图进行描述:

image-20210307163106883

12.4.2.2 AtomicStampedReference

AtomicStampedReference原子类是一个带有时间戳的对象引用更新(实际上就是一个版本,当版本发生变化时说明已经有其他线程进行了操作)

在每次修改之后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间戳

当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境

那么下面对AtomicStampedReference类进行测试:

public class Demo07 {
    static AtomicInteger atIn = new AtomicInteger(100);

    //初始化时需要传入一个初始值和初始时间
    static AtomicStampedReference<Integer> atomicStampedR =
            new AtomicStampedReference<Integer>(200,0);


    static Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            //更新为200
            atIn.compareAndSet(100, 200);
            //更新为100
            atIn.compareAndSet(200, 100);
        }
    });


    static Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag=atIn.compareAndSet(100,500);
            System.out.println("flag:"+flag+",newValue:"+atIn);
        }
    });


    static Thread t3 = new Thread(new Runnable() {
        @Override
        public void run() {
            int time=atomicStampedR.getStamp();
            //更新为200
            atomicStampedR.compareAndSet(100, 200,time,time+1);
            //更新为100
            int time2=atomicStampedR.getStamp();
            atomicStampedR.compareAndSet(200, 100,time2,time2+1);
        }
    });


    static Thread t4 = new Thread(new Runnable() {
        @Override
        public void run() {
            int time = atomicStampedR.getStamp();
            System.out.println("sleep 前 t4 time:"+time);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag=atomicStampedR.compareAndSet(100,500,time,time+1);
            System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference());
        }
    });

    public static  void  main(String[] args) throws InterruptedException {
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        t3.start();
        t3.join();
        t4.start();
        t4.join();
    }
}

测试方案的基本逻辑为:

这里分别创建了AtomicInteger和AtomicStampedReference进行对比

我们知道线程t1一定先执行且执行完成之后才会将t2线程处于就绪状态,那么t1线程更新变量为200,之后在更新为100,然后t2线程再进行CAS操作

而对于t3线程和t4线程实际上基本逻辑相同,只是使用AtomicStampedReference类为原子更新增加了版本

测试结果

image-20210307164445550

这里可以看出AtomicStampedReference类避免了ABA问题


这里要对AtomicStampedReference类进行下说明: image-20210307165004082

这里的时间戳变量stamp是一个int类型变量,且stamp并不是自动设置,而是在每次进行CAS操作时主动更新stamp变量的值,当然如果修改成功版本就发生了改变,反之版本不会改变

1.4.4 原子类实现自旋锁

自旋锁是一种假设在不久将来当前线程可以获取到锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这里也是称为自旋的原因),在经过若干次循环之后,如果得到锁,就顺利进入临界区;如果还不能获取到锁,那就会将线程在操作系统层面挂起,这种方式的确可以提升效率

然而当线程越来越多且竞争越来越激烈,占用CPU的时间变长导致性能急剧下降,故Java虚拟机内部一般对于自旋锁有一定的次数限制,可能是50到100次循环后放弃,直接挂起线程让出CPU资源


下面通过AtomicReferenece实现简单的自旋锁:

public class SpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread currentThread = Thread.currentThread();
        while (!sign.compareAndSet(null, currentThread)) {

        }
    }

    public void unlock() {
        Thread currentThread = Thread.currentThread();
        while (!sign.compareAndSet(currentThread, null)) {
            
        }
    }
}

这里使用CAS原子操作作为底层实现,lock()方法将需要更新的值设置为当前线程,将预期值设置为null

unlock()函数将要更新的值设置为null,并预期值设置为当前线程

然后我们通过lock()unlock()方法控制自旋锁的开启和关闭,注意这是一种非公平锁

事实上AtomicInteger(或者AtomicLong)原子类内部的CAS操作也是通过不断的自循环(while循环)实现,不过这种循环的结束条件是线程成功更新对于的值,但也是自旋锁的一种


测试自定义的自旋锁:

public class Demo007 {
    private static SpinLock lock = new SpinLock();

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                lock.lock();
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(new Date());
        lock.lock();
        System.out.println(new Date());
        System.out.println("已经获取到锁");
    }
}

这里我们知道在自定义的自旋锁中并没有用到锁,那么这里是如何实现的呢?

我们首先创建了一个自旋锁对象

对于线程t1首先调用lock.lock()方法,由于我们自定义的SpinLock类中维护了一个AtomicReference<Thread>类型的变量sign,而在类初始化时调用AtomicReference<Thread>的构造器,而构造器中又没有传入任何参数,那么对于AtomicReference<Thread>类中的value属性也就是null

image-20210307171128621

进而通过CAS操作判断当前value变量是否为null,如果是就修改为当前线程,很明显线程t1修改成功(因为mian线程休眠了1s)

当main线程尝试调用lock.lock()方法时,进行CAS操作时发现这时的值已经经过修改,所以自身会进行自旋操作

这就是实现自旋锁的原理,只有当线程t1通过lock.unlock()方法重新通过CAS操作将变量value从当前线程设置为null时,mian线程才会停止自旋并向下执行

测试结果

image-20210307171611924

从时间上可以看出main线程是在t1线程执行完成之后才开始执行

2 共享模型-不可变

2.1 日期转换的问题

我们平时在进行日期格式进行转换时会使用SimpleDateFormat类,那么在并发环境下该类是否安全呢?

下面对此进行一个简单的测试:

public class Demo8 {
    private static SimpleDateFormat format = new SimpleDateFormat("YYYY-MM-dd");
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    Date date = format.parse("2021-03-04");
                    log.info("{}",date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

测试结果

image-20210307172709431


既然上面出现了问题,我们应该如何进行改进呢?

这里可以使用DateTimeFormatter类替代SimpleDateFormat

其实从源码的注释上也可以发现DateTimeFormatter类不可变且线程安全

image-20210307173342847

下面进行测试:

@Slf4j
public class Demo8 {
    private static DateTimeFormatter format = DateTimeFormatter.ofPattern("YYYY-MM-dd");
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                TemporalAccessor accessor = format.parse("2021-03-04");
                log.info("{}",accessor);
            }).start();
        }
    }
}

测试结果

image-2021030717340687413.2 SimpleDateFormat线程不安全原因及解决方案

2.2.1 SimpleDateFormat线程不安全原因

2.2.2 解决方案

2.3 不可变对象设计

对于String类我们非常熟悉,下面就以String类来对不可变进行设计相关的要素

image-20210307174303158

可以看出String类中存在两个属性,其中一个就是char数组用来保存创建字符串时使用额字节数组,而另外一个hash就是用来缓存字符串对象的hashcode

对于final关键字:

  • final修饰的属性用来保证该属性仅可读,但是不能进行修改
  • final修饰的类保证了该类中的方法不能被重写,防止子类无意间破坏其不可变性(实际上就是final修饰的类隐式的将其中的方法默认添加了final关键字),同时final修饰的类不可被继承

下面再来看下String类的构造方法:

image-20210307175001894

首先来理解下注释的意思:

复制字节数组中的内容,之所以复制是因为当原始数组被修改时并不影响String对象中的value属性值

再来看下构造器:

这里实际上就是将原有的字节数组进行拷贝之后才赋值给value属性,从而说明了传入的参数无论如何改变也不会影响String对象内部的结构


接着再来看下substring方法:

image-20210307175342594

首先先检查截取的长度是否符合规则,如果符合就调用String的构造方法,而我们知道String的构造方法实际上就是通过拷贝字节数组实现

所以无论截取之前字符串的字节数组如何改变,也不会影响截取之后的字符串内容


其实这里就体现了不可变对象设计的一种理念:保护性拷贝

在创建新的String对象时会生成新的字节数组,实际上就是对原有字节数组进行拷贝,这种通过创建副本对象来避免发生共享问题的思想就是保护性拷贝

2.4 享元模式

在面向对象设计过程中,有时需要面临创建大量相同和相似对象实例的问题。创建那么多相似的对象会耗费很多的性能

2.4.1 定义和特点

定义

运用共享技术来有效地支持大量细粒度对象的复用,通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似类的开销,从而提高系统资源的利用率

优点

相同对象只保存一份,降低了系统中该对象的数量,降低了系统中细粒度对象给内存带来的压力

缺点

为了使对象可以共享,需要将一些不能共享的状态外部化,增加系统的复杂度

读取享元模式的外部状态使得运行时间稍微变长

2.4.2 结构

享元模式的定义中提出两个要求:细粒度和共享对象

因为要求细粒度,所以不可避免地会使对象数量多且性质相近,因此需要对对象的信息分为两个部分:内部状态和外部状态

  • 内部状态指对象共享出来的信息,存储在享元信息内部,并且不会随环境的改变而改变
  • 外部状态指对象得以依赖的一个标记,随环境改变而改变,不可共享

比如连接池中的连接对象,保存在连接对象中的用户名、密码,连接URL等信息,在创建对象的时候就已经设置完毕,不会随环境改变而改变,这些为内部状态

当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态


享元模式中的主要角色

  • 抽象享元角色:是所有的具体享元类的基类,为定义享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法注入
  • 具体享元角色:实现抽象享元角色中所规定的接口
  • 非享元角色:不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中
  • 享元工厂角色:负责创建和管理享元角色,当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,如果不存在则创建一个新的享元对象

享元模式结构图

image-20210307184452381

  • UnsharedConcreteFlyweight是非享元角色,里面包含了非共享的外部状态信息info
  • Flyweight是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入
  • ConcreteFlyweight是具体享元角色,包含了关键字key,它实现了抽象享元接口
  • FlyweightFactory是享元工厂角色,它是关键字key来管理具体享元
  • 客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法

13.4.3 模式实现

抽象享元角色

public interface Flyweight {
    /**
     * 注入非享元角色的外部状态
     */
    void operation(UnshareConcreteFlyweight outState);
}

非享元角色

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UnshareConcreteFlyweight {
    private String info;
}

具体享元角色

@Slf4j
public class ConcreteFlyweight implements Flyweight{
    private String key;

    public ConcreteFlyweight(String key) {
        this.key = key;
        log.info("具体享元{}被创建",key);
    }

    @Override
    public void operation(UnshareConcreteFlyweight outState) {
        log.info("具体享元{}被调用",key);
        log.info("非享元信息是:{}",outState.getInfo());
    }
}

享元工厂角色

@Slf4j
public class FlyweightFactory {
    private HashMap<String,Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String key) {
        Flyweight flyweight = flyweights.get(key);
        if (flyweight != null) {
            log.info("享元{}已存在,被成功获取",key);
        } else {
            flyweight = new ConcreteFlyweight(key);
            flyweights.put(key,flyweight);
        }
        return flyweight;
    }
}

客户端测试

public class FlyweightPattern {
    public static void main(String[] args) {
        FlyweightFactory factory = new FlyweightFactory();
        Flyweight f01 = factory.getFlyweight("a");
        Flyweight f02 = factory.getFlyweight("a");
        Flyweight f03 = factory.getFlyweight("a");
        Flyweight f11 = factory.getFlyweight("b");
        Flyweight f12 = factory.getFlyweight("b");
        f01.operation(new UnsharedConcreteFlyweight("第1次调用a"));
        f02.operation(new UnsharedConcreteFlyweight("第2次调用a"));
        f03.operation(new UnsharedConcreteFlyweight("第3次调用a"));
        f11.operation(new UnsharedConcreteFlyweight("第1次调用b"));
        f12.operation(new UnsharedConcreteFlyweight("第2次调用b"));
    }
}

测试结果

image-20210307190452768

从内存地址中可以看出只有第一次获取的时候才会被创建,之后如果存在就直接获取

image-20210307190545236

2.4.4 Integer包装类中的享元模式

首先我们先通过小测试来引入Integer:

@Slf4j
public class IntegerTest {
    public static void main(String[] args) {
        Integer a = Integer.valueOf(127);
        Integer b = 127;

        Integer c = Integer.valueOf(128);
        Integer d = 128;

        log.info("{}", a == b);
        log.info("{}",c == d);
    }
}

测试结果:

image-20210307191305496

可以看出通过127创建的包装类不管怎样都是一个对象,而128创建的包装类每次都是新的对象


源码分析

image-20210307191428029

这里可以看到首先判断参数i是否在IntegerCache.lowIntegerCache.high之间,如果在就从IntegerCache类中的cache数组中取出该对象

如果没有就新建一个包装类对象

那么这里的IntegerCache到底是什么呢?


/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

IntegerCache是Integer类的内部类

我们先理解下IntegerCache类的注释:

IntegerCache默认对-128-127中的整数进行缓存,但是可以通过JVM的启动项进行修改最大的缓存值,启动指令为java.lang.Integer.IntegerCache.high

然后对静态代码块进行分析:

这里先创建了一个变量h为127,然后获取JVM启动向中java.lang.Integer.IntegerCache.high指令的值

如果该值存在就取127和该指令获取的值之间的最大值赋值给h,然后在把h赋值给high变量(这里已经实现了自定义缓存的最大值)

然后通过遍历创建缓存数组


结合上面的分析,我们知道当调用Integer#valueOf方法时调用了IntegerCache中的静态属性,那么一定对IntegerCache进行了初始化,也就调用了静态代码块

从而可以说通为什么当创建值为127的包装对象时实际上都是同一个对象

2.4.5 自定义连接池

我们这里首先通过实现Connection接口自定义一个连接类

@ToString
class MockConnection implements Connection {

    @Override
    public Statement createStatement() throws SQLException {
        return null;
    }
....
}

然后定义自定义连接池

@Slf4j
public class Pool {
    // 连接池大小
    private int poolSize;
    // 连接对象数组
    private Connection[] connections;
    // 连接状态数组,0表示未使用,1表示使用
    private AtomicIntegerArray states;

    // 构造方法
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        connections = new Connection[poolSize];
        states = new AtomicIntegerArray(poolSize);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection();
        }
    }

    // 获取连接
    public Connection getConnection() {
        while (true) {
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    states.compareAndSet(i,0,1);
                    log.info("get {}",connections[i]);
                    return connections[i];
                }
            }
            // 如果没有空闲连接则先阻塞
            synchronized (this) {
                try {
                    log.info("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 返回连接
    public void free(Connection connection) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == connection) {
                states.set(i,0);
                log.info("free {}",connections[i]);
                break;
            }
        }
        synchronized (this) {
            this.notifyAll();
        }
    }

}

定义连接池

对应states属性因为数组线程不安全,因此需要使用AtomicIntegerArray来保证同时修改数组中的值不会出现并发问题

当调用构造器时通过传入的参数指定连接池的大小,并且通过连接池大小来创建连接对象数组和连接对象数组,可以看出连接对象数组中的每个对象都是MockConnection对象

定义获取连接

首先遍历整个连接池,如果存在状态为0的线程对象就先通过CAS修改连接状态信息,然后获取连接

如果没有空闲连接,就将当前线程阻塞等待其他线程释放连接之后唤醒

定义释放连接

首先判断释放的连接是否是连接池中的连接对象,如果是就设置连接状态为1(因为这个连接对象是被某个线程所获取,其他线程已经无法获取该连接,因此不存在安全问题)

当释放连接之后,就唤醒当前处于阻塞状态的线程


测试类

public class Test {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection connection = pool.getConnection();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(connection);
            }).start();
        }
    }
}

测试结果

image-20210307200330019

这里可以看出只有两个线程可以获取连接,只有当其中的连接释放之后才能重新获取连接


细节

  • 实际上现在代码依然存在问题,连接状态数组需要使用volatile关键字进行修饰,为了保证该变量的可见性(不然如果某些线程已经修改了状态,然而主存中并没有更新,那么依然会出现并发错误)

    image-20210307200538042

  • 之所以在获取连接时使用while循环,是因为当释放连接的线程唤醒阻塞线程时必须要保证再次检查连接池中是否存在连接(因为有可能已经又被其他线程获取)


以上实现方式没有考虑:

  • 连接的动态增长和收缩

  • 连接保活(可用性检测)

    当由于某些原因导致连接失效时,无法检测该连接是否有效(比如心跳检测)

  • 等待超时处理

    我们为了防止当获取连接时没有获取到就一直进行循环尝试,使用this.wait()来阻塞该线程,然而并没有对阻塞时间进行处理

  • 分布式hash


个人公众号目前正初步建设中,如果喜欢可以关注我的公众号,谢谢!

二维码