学习笔记 1.高性能编程 1.2.2 线程安全之原子操作

原子操作:

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割,而只执行其中的一部分(不可中断性)。将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

保证线程原子性的方法:

  1. synsynchronize关键字(单线程);
  2. 加锁(单线程);
  3. AtomicInteger类作为成员变量(原子性的Integer);

原子性操作的原理及实现步骤:

首先需要介绍CAS(Compare and swap),Compare and swap比较和交换。属于硬件同步原语,处理器提功了基本内存操作的原子性保证。CAS操作需要输入两个数值,一个旧值(期望操作前的值)A和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换,交换失败后进行自旋,直到成功或者线程失败结束。Java中的sun.misc.Unsafe类,提供了compareAndSwapint()和compareAndSwapLong()等几个方法实现CAS。

操作系统修改内存中的值通常是经过找到变量在内存中的内存地址,然后修改变量的值,但是作为开发人员,我们是在JVM下编写代码,Java的API是不允许我们像操作系统一样去通过内存地址去找到变量,修改变量的值。通常我们是使用上面所提到的sun.misc.Unsafe类,Unsafe知道到每个对象在内存中的内存区域是怎样的,并且可以得到对应字段的offset,也就是我们所说的偏移量,偏移量的类型是Long类型。下面是通过Unsafe,简单的实现CAS操作

public class CounterUnsafe {

    volatile int i = 0;

    private static Unsafe unsafe = null;
    //偏移量
    private static Long valueOffset;

    static {
        try {
            //通过反射得到Unsafe类
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //获取i字段的偏移量
            Field fieldi = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(fieldi);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add() {

        for (;;){
            //通过偏移量取到变量值
            int current = unsafe.getIntVolatile(this,valueOffset);
            //若CAS操作成功,则停止自旋
            if(unsafe.compareAndSwapInt(this, valueOffset, current, current+1)) {
                break;
            }
        }

    }
}

J.U.C包内的原子原子操作封装类:

AtomicBoolean:原子更新布尔类型;

AtomicInteger:原子更新整型;

AtomicLong:原子更新长整型;

AtomicIntegerArray:原子更新整型数组里的元素;

AtomicLongArray:原子更新长整型数组里的元素;

AtomicReferenceArray:原子更新引用类型数组里的元素;

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器;

AtomicLongFieldUpdater:原子更新长整型的字段的更新器;

AtomicReferenceFieldUpdater:原子更新 引用类型里的字段;

AtomicReference:原子更新引用类型;

AtomicStampedReference:原子更新带有版本号的引用类型;

AtomicMarkableReference:原子更新带有标记号的引用类型;

1.8更新:计数器增强版,高并发下性能更好

更新器:DoubleAccumulator、LongAccumulator

计数器:DoubleAdder、LongAdder

原理:分成多个操作单元,不同线程更新不同的单元只有需要汇总的时候才计算所有单元的操作。

适用场景:高并发频繁更新,不太频繁地读取。

区别:Accumulator内部需要使用lamdba表达式来实现计数逻辑,Adder则只是简单的调用。

// LongAdder 方式
    public long testLongAdder() throws InterruptedException {
        LongAdder lacount = new LongAdder();
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    lacount.increment();
                }
                long endtime = System.currentTimeMillis();
            }).start();
        }
        Thread.sleep(3000);
        return lacount.sum();
    }

    public long testAccumulator() throws InterruptedException {
        LongAccumulator accumulator = new LongAccumulator((x, y)->{
            return x + y;
        }, 0L);

        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    accumulator.accumulate(1);
                }
                long endtime = System.currentTimeMillis();
            }).start();
        }
        Thread.sleep(3000);
        return accumulator.get();
    }

CAS存在的三个问题:

  1. 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗
  2. 仅针对单个变量的操作,不能同时用于多个变量来实现原子操作,比如现在内存中有多个变量CPU无法通过CAS操作来实现原子性。
  3. ABA问题(重点)。

下面详细讲解ABA问题:

  1. 线程1、线程2同时读取到变量i的值为0;
  2. 两个线程同时进行CAS操作将变量i改为1;
  3. 若线程2的操作十分缓慢,在线程1执行完CAS(0,1)后紧接着执行完CAS(1,0);
  4. 线程1执行完两次CAS操作后,线程2再次执行后会显示成功,未达到预期效果,这就是ABA问题;

ABA问题就是虽然内存中值为发生变化,但是其版本不同,实际是发生了变化的,此时CAS应该失败,为此在CAS 方法中应该添加版本号,比较值的时候同时比较版本号

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

线程安全的概念

竞态条件:如果程序运行顺序的改变会影响最终结果,就说存在竞态条件。大多数竞态条件的本质,就是基于某种可能失效的观察结果来做出判断,或执行某个计算。

临界区:存在竞态条件的代码区域叫临界区。

共享资源:

只有当多个线程更新共享资源时,才会发生竞态条件,可能会出现线程安全问题。

栈封闭时,不会在线程之间共享的变量,都是线程安全的。

局部对象引用本身不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其它线程可用,那么也是线程安全的。

不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。

实例被创建,value变量就不能再被修改,这就是不可变性。

使用ThreadLocal,相当于不同的线程操作的是不同的资源,所以不存在线程安全问题。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值