无锁并发-CAS与原子类

CAS

CAS即是一种典型的乐观锁(无锁)思想,它的原理是:在修改共享变量之后,先对共享变量的最新值进行一个比对,如果最新值和旧值是一样的,说明对共享变量操作这段时间没有其他线程修改变量,就可以将新值赋给共享变量;否则就需要重新取得最新值来重新计算,为了保证变量的可见性,需要使用volatile修饰共享变量

CAS与synchronized效率对比

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇;而synchronized的悲观锁思想,会让线程在没有获得锁的时候,发生上下文切换,上下文切换时线程的阻塞和恢复都需要在内核态来执行
  • 在无锁情况下,因为线程一直保持运行,需要额外的CPU支持,会消耗大量的时间片,如果没有分到时间片,虽然不会进入阻塞态,但仍然会导致上下文切换;而且频繁的失败重试会造成大量CPU资源的无端浪费

CAS特点

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,出现了修改就失败重试
  • synchronized是基于悲观锁的思想:最悲观的估计,在自己占有锁的时候其他线程不能同时修改共享变量,只有解完了锁其他线程才有机会
  • CAS体现的是无锁并发、无阻塞并发:没有加锁,如果不会陷入阻塞;但如果竞争激烈,频繁的重试反而会让效率受影响

CAS的实现(以AtomicInteger为例)

@Slf4j
public final class Demo{
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                add(1);
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                add(-1);
            }
        },"t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.debug("ans={}", atomicInteger.get());
    }
    private static void add(int num){
        // 在循环块中进行操作
        while(true){
            // 取得旧值
            int prev = atomicInteger.get();
            // 对旧值进行计算
            int update = prev+num;
            // compareAndSet方法底层调用的是compareAndSwap方法,比对旧值是否被改变,没有被改变就更新数据并且跳出循环
            if(atomicInteger.compareAndSet(prev,update)){
                break;
            }
        }
    }
}

原子类

JUC(java.util.concurrent)并发包提供了原子整数、原子引用、原子数组等原子类,内部实现就是基于CAS无锁并发

原子整数

AtomicInteger原子整型、AtomicLong原子长整型、AtomicBoolean原子布尔型,内部方法基本相同,内部提供了部分已经封装好的方法供开发者使用,同时也可以使用提供的CAS方法来自己实现CAS操作

方法功能
getAndIncrement、incrementAndGet原子自增
getAndDecrement、decrementAndGet原子自减
getAndAdd、addAndGet原子增加
getAndUpdate、updateAndGet可传入一个lambda表达式来作复杂的原子计算,底层原理是传入一个函数式接口类型,来满足灵活性和复用性
getAndAccumulate、accumulateAndGet累加操作
compareAndSet底层调用compareAndSwap(CAS)方法,可以自己实现CAS操作

原子引用

  • AtomicReference:原子更新引用
  • AtomicStampedReference:原子更新带有版本号的引用类型,该类将整数数值与引用关联起来,可用于原子的更新数据和数据的版本号,解决ABA问题
  • AtomicMarkableReference:芋艿更新带有标记位的引用类型,可以原子更新一个布尔类型的标记位和引用类型

ABA问题的解决

ABA问题

@Slf4j
public final class Demo{
    private static AtomicReference<String> atomicString = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        String prev = atomicString.get();
        String update = "C";
        // 将原子引用两次修改
        update("B");
        update("A");
        Thread.sleep(1000);
        log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update));
    }

    private static void update(String update){
        String prev = atomicString.get();
        log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update));
    }

}

在这里插入图片描述
如果修改过程中,有一个线程将原子引用修改两次,当前线程是无法感知

使用AtomicStampedReference解决ABA问题

@Slf4j
public final class Demo{
    private static AtomicStampedReference<String> atomicString = new AtomicStampedReference<>("A",0);
    public static void main(String[] args) throws InterruptedException {
        // 获取共享变量值
        String prev = atomicString.getReference();
        // 获取版本号
        int stamp = atomicString.getStamp();
        String update = "C";
        // 将原子引用两次修改
        update("B");
        update("A");
        Thread.sleep(1000);
        log.debug("{}->{},stamp:{},{}",prev,update,stamp,atomicString.compareAndSet(prev,update,stamp,stamp+1));
    }

    private static void update(String update){
        String prev = atomicString.getReference();
        int stamp = atomicString.getStamp();
        log.debug("{}->{},stamp:{},{}",prev,update,stamp,atomicString.compareAndSet(prev,update,stamp,stamp+1));
    }

}

在这里插入图片描述
在改动时,不光基于共享变量的值,还需要比较版本号,这样根据版本号就可以追踪原子引用的变化次数

使用AtomicMarkableReference解决ABA问题

@Slf4j
public final class Demo{
    private static AtomicMarkableReference<String> atomicString = new AtomicMarkableReference<>("A",true);
    public static void main(String[] args) throws InterruptedException {
        // 获取共享变量值
        String prev = atomicString.getReference();
        String update = "C";
        // 将原子引用两次修改
        update("B");
        update("A");
        Thread.sleep(1000);
        log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update,true,false));
    }

    private static void update(String update){
        String prev = atomicString.getReference();
        log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update,true,false));
    }

}

在这里插入图片描述
AtomicMarkableReference只使用一个布尔值做一个标记,可以用来保证某个操作只进行一次,当标记发生改变时,就不再进行操作,他不会统计改变的次数,而只是检测是否改变

原子数组

  • 使用原子引用来包装数组只能保护数组的引用的线程安全性,但是无法保证数组内部数据的线程安全性,所以需要使用到原子数组
  • AtomicIntgerArray:原子整型数组
  • AtomicLongArray:原子长整型数组
  • AtomicReferenceArray:原子引用类型数组

数组的线程安全性通用测试方法

@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        // 检测普通数组的线程安全性
        test(
                ()->new int[10],
                (array)->array.length,
                (array,index)->array[index]++,
                (array)->System.out.println(Arrays.toString(array))
        );

        // 检测原子数组的线程安全性
        test(
                ()->new AtomicIntegerArray(10),
                (array)->array.length(),
                (array,index)->array.getAndIncrement(index),
                (array)-> System.out.println(array)
        );
    }

    /***
     * supplier-提供者,无参数有结果,用于提供数据
     * function-函数,有参数有结果;一个参数一个结果称为Function,两个参数一个结果称为BiFunction
     * consumer-消费者,有参数无结果,对数据作处理,一个参数称为Consumer,两个参数称为BiConsumer
     *
     * 数组的线程安全性检测通用测试方法
     * @param arraySupplier         提供需要检测的数组
     * @param lengthFun             提供获取数组长度的方法
     * @param putConsumer           数组操作的方法
     * @param printConsumer         打印数组的方法
     * @param <T>                   数组类型
     */
    private static <T> void test(
            Supplier<T> arraySupplier,
            Function<T,Integer> lengthFun,
            BiConsumer<T,Integer> putConsumer,
            Consumer<T> printConsumer
    ){
        List<Thread> threads = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            threads.add(new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    // 将一万次数据操作均摊到每个数组元素上
                    putConsumer.accept(array,j%length);
                }
            }));
        }
        // 开启所有线程
        threads.forEach(thread -> thread.start());
        // 等待所有线程结束
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 打印数组
        printConsumer.accept(array);
    }
}

在这里插入图片描述

字段更新器

  • 原子引用只能原子的修改引用的指向,不能修改引用的对象内部的属性,对内部的属性需要使用字段更新器来处理
  • AtomicReferenceFieldUpdater:引用类型字段更新器
  • AtomicIntegerFieldUpdater:整型字段更新器
  • AtomicLongFieldUpdater:长整型字段更新器
@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        Student student = new Student("Jack");
        // 为字段绑定更新器
        AtomicReferenceFieldUpdater<Student,String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
        // 更新器的方法和其他原子类基本一致
        updater.compareAndSet(student,"Jack","Tom");
        log.debug("{}",student);
    }

}
class Student {
   volatile String name;

   public Student(String name){
       this.name =name;
   }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

原子累加器

除了可以使用原子基本类型的getAndIncrement方法,还有特定的原子累加器如LongAddr、DoubleAddr,可以提供更高性能的累加;性能提升的原因是在有竞争时,设置多个累加单元,每个线程进行自己的累加操作,最后将多个线程累加操作汇总,因此减少了CAS重试失败

LongArr与AtomicLong对比

@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            test(
                    ()->new LongAdder(),
                    longAdder -> longAdder.increment()
            );
        }
        log.debug("");
        for (int i = 0; i < 5; i++) {
            test(
                    ()->new AtomicLong(0),
                    atomicLong->atomicLong.getAndIncrement()
            );
        }



    }

    private static <T> void test(
            Supplier<T> supplier,
            Consumer<T> action
    ){
        T addr = supplier.get();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            threads.add(new Thread(()->{
                action.accept(addr);
            }));
        }
        long start = System.nanoTime();
        threads.forEach(thread -> thread.start());
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();

        log.debug(addr+"用时{}",(end-start)/1000_000);
    }

}

LongAddr源码分析

LongAddr类中有几个关键域:

// 累加单元数组,懒惰初始化
transient volatile Cell[] cells;

// 基础值,如果没有竞争,就用cas累加这个域
transient volatile long base;

// 在cells创建或扩容时,置为1,表示加锁;结合cas和该标志位实现cas加锁
transient volatile int cellsBusy;

cas锁

public class LockCas{
	// 标志位,为0时表示没有加锁,为1时表示加锁
	private AtomicInteger state = new AtomicInteger(0);
	
	// 加锁操作,通过cas来修改标志位,只有标志位为0时才可以修改成功,符合没有加锁时才能加锁的特点
	public void lock(){
		while(true){
			if(state.compareAndSet(0,1)){
				break;
			}
		}
	}

	// 解锁操作,将标志位修改为0
	public void unlock(){
		state.set(0);
	}
}

CAS锁不推荐在生产开发中使用,虽然底层使用到了这个方式;加锁的场景一般是为了应对高并发场景,而CAS本就不适合大量线程竞争的场景

累加单元类Cell

// 该注解防止缓存行伪共享
@sun.misc.Contended
static final class Cell{
	volatile long value;
	Cell(long x) {value = x;}
	// 最重要的方法,用cas的方式来进行累加,prev表示旧值,next表示新值
	final boolean cas(long prev,long next){
		return UNSAFE.compareAndSwapLong(this,valueOffset,prev,next);
	}
	// 省略不重要代码
}

伪共享
在这里插入图片描述

  • 因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率
  • 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)
  • 缓存的加入虽然中和了CPU和内存之间的速度差异,但是导致了数据副本的产生,即不同CPU核心对应不同的缓存行,同一份数据会缓存在不同核心的缓存行中,如果其中一个核心对数据进行了修改,不同缓存行的数据就不一致了
  • CPU要保证数据的一致性,如果某个CPU核心修改了数据,其他CPU核心对应的整个缓存行必须失效

在如上缓存行的基础上,回到cells数组的累加操作:
在这里插入图片描述
一个Cell对象的大小约是16byte对象头+8byte长整型,即24byte;所以一个缓存行(64byte)能够放下Cell数组中的两个Cell对象,假设此时核心1需要修改Cell[0],核心2需要修改Cell[1],那么无论谁修改成功,都会导致另一个核心的缓存行失效,需要重新从内存读取,从而使性能受到影响,这个缓存行失效的问题就称为伪共享

@sun.misc.Contended就用来解决这个缓存行失效的问题,它的原理是在使用此注解的对象或字段前后各增加128字节大小padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样就不会造成对方的缓存行失效
在这里插入图片描述

该注解是Java8提供的,在jdk1.7及以前,通常通过手动地前后填充的方式来避免伪共享:

public volatile long value = 0;

改为

volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0;
volatile long q0, q1, q2, q3, q4, q5, q6;

累加操作源码分析
increment和decremnt方法底层都是调用add方法
在这里插入图片描述
add方法分析:
在这里插入图片描述
在这里插入图片描述
add方法主要做了对无竞争时使用base进行累加,和对已经创建Cell的线程进行累加操作,在累加的cas操作失败时,表示出现了竞争,则进入longAccumulate方法创建Cell

longAccumulate方法分析:
在这里插入图片描述

在这里插入图片描述
cells已经创建的情况:
在这里插入图片描述

在这里插入图片描述

cells未创建且cellsBusy加锁成功的情况:
在这里插入图片描述
获取累加最终结果:
在这里插入图片描述

Unsafe

Unsafe对象提供了非常底层的,操作内存、线程的方法;Unsafe时非常底层的类,它的对象不能直接创建(不推荐开发者使用),只能通过反射获得

获取Unsafe对象

public class UnsafeAccessor{
	static Unsafe unsafe;
	static {
		try{
			Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
			theUnsafe.setAccessible(true);
			unsafe = (Unsafe) theUnsafe.get(null);
		}catch(NoSuchFieldException|IllegalAccessException e){
			throw new Error(e);
		}
	}
}

使用unsafe进行cas操作

@Slf4j
public final class Demo{
    public static void main(String[] args) throws NoSuchFieldException {
        Unsafe unsafe = getUnsafe();
        long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));

        Student student = new Student();
        student.id = 1;
        student.name = "Jack";

        unsafe.compareAndSwapInt(student,idOffset,1,2);
        unsafe.compareAndSwapObject(student,nameOffset,"Jack","Tom");

        log.debug("{}",student);
    }

    private static Unsafe getUnsafe(){
        Unsafe unsafe;
        try{
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        }catch(NoSuchFieldException|IllegalAccessException e){
            throw new Error(e);
        }
        return unsafe;
    }

}

class Student {
    int id;
    String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

模拟实现原子整数

public class MyAtomicInteger {
    private volatile int value;
    static final Unsafe unsafe;
    static final long DATA_OFFSET;

    static {
        // 初始化Unsafe对象和字段偏移量
        try{
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            DATA_OFFSET = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("data"));
        }catch(NoSuchFieldException|IllegalAccessException e){
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public MyAtomicInteger(int value){
        this.value = value;
    }

    public int get(){
        return value;
    }

    public void decrement(){
        update(value-1);
    }

    public void increment(){
        update(value+1);
    }

    public void update(int next){
        while(true){
            int prev = value;
            if(unsafe.compareAndSwapInt(this,DATA_OFFSET,prev,next)){
                break;
            }
        }
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值