体系化深入学习并发编程(六)原子类和CAS

原子类

原子类是指JUC库里的atomic包下的类。
这些原子类都具有原子性(在JMM文章中有详细叙述)
提到原子,就会想到化学,在高中化学所学的相关的一些化学方程式中,原子是不可再分的(这里并不去探究中子、质子、夸克什么)
而我们所谓的原子性就是这个性质:不可再分

原子性可以保障线程安全,因为是不可再分,所以在同一时间,只能有一个线程能够正确操作,达到了线程安全的效果

相对于锁而言,原子类的粒度更细,锁保证的原子性的粒度通常一个临界区,有多个代码,而原子类可能将这个范围缩小的一个变量。
所以,原子类的性能也要优于锁(非高度竞争情况下)

原子基本类型

Java提供了三个基本类型的原子类:
AtomicBooleanAtomicIntegerAtomicLong

下面就用AtomicInteger举例
常用方法:

  • public final int get() //获取当前值
  • public final int getAndSet(int newValue) //获取当前值并设置为新值
  • public final int getAndAdd(int delta) //获取当前值并增加设定的值
  • public final int getAndIncrement() //获取当前值并自增
  • public final int getAndDecrement() //获取当前值并自减
  • public final boolean compareAndSet(int expect,int update) //如果当前值是expect,则将该值原子设置为update。
    //true表示成功,false表示当前值不等于expect

使用getAndIncrement()和普通基本类型的++操作进行对比

public class AtomicIntegerDemo {

    private static AtomicInteger atomicInt = new AtomicInteger(0);
    private static volatile int normalInt = 0;

    private static void addAtomicInt(){
        atomicInt.getAndIncrement();
    }

    private static void addNormalInt(){
        normalInt++;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            for (int i = 0; i <5000; i++) {
                addAtomicInt();
                addNormalInt();
            }
        };
        new Thread(r).start();
        new Thread(r).start();
        Thread.sleep(100);
        System.out.println("原子基本类型:"+atomicInt+" 基本类型:"+normalInt);
    }
}

打印结果:

原子基本类型:10000 基本类型:9780

原子数组类型

Java提供了三个数组类型的原子类:
AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
在数组内,每个数组元素都是具有原子性的
下面同样用AtomicIntegerArray来举例

public class AtomicArrayDemo {
    //创建一个1000容量的原子整型数组
    private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(1000);

    //每个元素自减1
    private static void decre(AtomicIntegerArray atomicIntegerArray){
        for (int i = 0; i <atomicIntegerArray.length(); i++) {
            atomicIntegerArray.getAndDecrement(i);
        }
    }
    //每个元素自增2
    private static void add(AtomicIntegerArray atomicIntegerArray){
        for (int i = 0; i <atomicIntegerArray.length(); i++) {
            atomicIntegerArray.getAndAdd(i,2);
        }
    }

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        threadPool.execute(()->add(atomicIntegerArray));
        threadPool.execute(()->decre(atomicIntegerArray));
        threadPool.shutdown();
        while (true){
            if (threadPool.isTerminated()){
                for (int i = 0; i <atomicIntegerArray.length(); i++) {
                    if (atomicIntegerArray.get(i)!=1){
                        System.out.println("出现错误");
                    }
                }
                System.out.println("完成");
                break;
            }
        }
    }
}

用100个线程的线程池来并发对数组每个元素进行操作,如果有一个数组的结果为1(初始为0,执行-1,+2操作),就打印出出现错误,但是由于原子性,并不会出现这种结果。

原子引用类型

Java提供了三个引用类型的原子类:
AtomicReferenceAtomicMarkableReferenceAtomicStampedReference

AtomicInteger和AtomicReference本质并无多少区别,不过前者是让一个整型保证原子性,而后者是让一个对象保证原子性。
由于对象的内部比整型更丰富,所以AtomicReference的功能也需要更强大,但是使用并无多少差别
在上一章锁的文章中,关于自旋锁的介绍时,我们使用到了这个类来实现了一把自旋锁

普通变量升级原子类型

AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
这三个类的作用是,将普通的变量,升级为具有原子性的变量。

有些时候,一个已经由普通变量定义好了的程序,需要增加一个方法,这个方法会遇到并发的场景,此时我们肯定不能全部推倒重来,这时就是使用这个类的时候。
同样,一个程序,只有少数时候需要原子性操作,其他时候并不需要考虑原子性,那么我们可以用普通的变量来节约操作成本,只需要在原子性操作时进行升级即可。

用升级前后的变量的自增操作来对比.

public class AtomicIntegerFieldUpdaterDemo {
    private volatile int a;
    //普通变量
    private static AtomicIntegerFieldUpdaterDemo normalDemo = new AtomicIntegerFieldUpdaterDemo();
    //升级变量
    private static AtomicIntegerFieldUpdaterDemo updaterDemo = new AtomicIntegerFieldUpdaterDemo();
	//获取updater
    private static AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterDemo> updater = AtomicIntegerFieldUpdater
            .newUpdater(AtomicIntegerFieldUpdaterDemo.class,"a");
	//两个变量都进行自增操作
    private static void increment(){
        for (int i = 0; i <5000; i++) {
            normalDemo.a++;
            updater.getAndIncrement(updaterDemo);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->increment()).start();
        new Thread(()->increment()).start();

        Thread.sleep(500);
        System.out.println("普通变量: "+normalDemo.a);
        System.out.println("升级变量: "+updaterDemo.a);
    }
}
普通变量: 9887
升级变量: 10000

值得注意的是,我们在调用AtomicIntegerFieldUpdater方法时,传递了两个参数,类名和变量名,很明显是通过反射调用的。

同样,该方法不支持static变量,会抛出初始化异常错误。

累加器Adder

JUC中提供了两个累加器:DoubleAdderLongAdder
累加器是JDK1.8后引入的,相比如普通的原子类,在多线程的情况下,普通原子类带来了性能上的瓶颈,而累加器在并发场景下的效率要更高,本质是通过空间换时间。

下面来对比高并发场景下AtomicLong和LongAdder的性能:

public class LongAdderDemo {
    private static AtomicLong atomicLong = new AtomicLong();
    private static LongAdder longAdder = new LongAdder();

    private static void atomicLongIncrement(){
        for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
            atomicLong.getAndIncrement();
        }
    }

    private static void longAdderIncrement(){
        for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
            longAdder.increment();
        }
    }

    private static long runTime(Runnable runnable){
        ExecutorService threadPool = Executors.newFixedThreadPool(16);
        long Start = System.currentTimeMillis();
        for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
            threadPool.submit(runnable);
        }
        threadPool.shutdown();
        while (!threadPool.isTerminated()){

        }
        long End = System.currentTimeMillis();
        return End-Start;
    }

    public static void main(String[] args) {
        System.out.println("AtomicLong  耗时:"+runTime(()->atomicLongIncrement())+"  结果:"+atomicLong.get());
        System.out.println("LongAdder  耗时:"+runTime(()->longAdderIncrement())+"  结果:"+longAdder.sum());
    }
}
AtomicLong  耗时:23627  结果:1073676289
LongAdder  耗时:4453  结果:1073676289

可以看到性能差距很明显。

为什么会出现这样的性能差异呢
这就和Java内存模型有关了
在JMM文章中关于谈到JMM的共享内存和本地内存时,也提到了计算机底层硬件的一些知识。
其中包括了冲刷处理器缓存刷新处理器缓存
AtomicLong多线程情况下的自增就是基于这两个操作。

线程1每次进行自增操作时,需要从共享内存中读取变量到本地内存,也就是refresh操作。
当线程1执行完自增后,需要把本地内存的变量写回共享内存,也就是flush操作。
同理其他线程也是如此

所以每次操作都需要进行同步,在高并发场景下,冲突带来了性能的降低

LongAdder则不一样,每个线程在自己的本地内存有一个变量,比如线程1有val1,线程2有val2,线程不去共享内存读取和刷新,只在自己的工作内存对自己的val进行自增操作。
LongAdder采取的是分段累加的策略
类似JDK1.7 ConcurrentHashMap里分段锁的概念
这也是最后调用的是LongAdder的sum方法:

public long sum() {
    Cell[] as = cells; Cell a;
	long sum = base;
	if (as != null) {
		 for (int i = 0; i < as.length; ++i) {
		 if ((a = as[i]) != null)
			 sum += a.value;
		 }
	}
	 return sum;
}

里面有两个值得注意的点:Cellbase

  • base变量:如果竞争不激烈,就直接累加在这个变量上
  • Cell[]:如果竞争激烈,每个线程分段累加在自己所占的数组槽上,通过hash将每个线程分别对应一个槽,不会对其他线程造成干扰。

总结
在低争用场景下,AtomicLong和LongAdder差别并不大。当并发量高了时,LongAddert用用空间换时间,提升了性能,但是多消耗了空间。
同时,LongAdder使用场景稍局限,通常适用于计数、求和方面,提供的都是加减等操作。而AtomicLong功能更丰富,还具有cas方法。

CAS

CAS(Compare and Swap)是一种算法思想,也是乐观锁的实现方式。同时这也是一个CPU指令。上面的原子类就用到了CAS操作。
CAS是用于并发场景的,常见于原子类、并发容器。
它只关心三个值:内存值V预期值A修改值B
首先它会去比较内存中的V和我所预期的值A是否相等,如果相等,就修改为B。否则就不操作。
CAS指令由CPU保证了对共享变量操作的原子性,但并没有保证可见性。
CAS是一个“if-then act”操作,我们可以通过代码来模拟这一指令

public class CAS {
    private int value;

    public synchronized int compareAndSwap(int except,int update){
        if (except==value){
            value=update;
        }
        return value;
    }
}

Java是如何使用到CAS来实现原子操作的呢?
Java提供了一个Unsafe类,来直接操作内存数据
该类是CAS的核心类,提供了硬件级别的原子操作
下面用AtomicInteger为例

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;//变量在内存中的偏移地址

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));//通过反射获取value值的内存偏移地址
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;//volatie保证可见性

CAS并不是尽善尽美,它也有缺点

  • ABA问题
  • 自旋问题

因为CAS是对值进行比较
如果线程1将值从0改为1,
线程2又把值改回0。
第三个线程过来发现值是0,会认为没有线程进行修改过,在后续的操作可能会出现错误。
同样,有时CAS的操作是需要长时间自旋操作,在自旋锁的实现时我们就通过自旋CAS操作来实现,而长时间的空自旋会导致CPU资源的浪费。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值