多线程之原子操作类|8月更文挑战

263 阅读8分钟

CAS (CompareAndSwap 比较和交换)

定义

​ CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

缺点:CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。(CPU 开销大)
  2. 只能保证一个共享变量的原子操作。(多个使用锁)
  3. ABA问题。(不影响使用可不考虑)
AtomicInteger i = new AtmoicInteger();
i.incrementAndGet();//自增并返回结果
 
//incrementAndGet() 方法跟踪进去会发现底层是调用的native方法 ( Unsafe.class类中的 
//public final native boolean compareAndSwapInt(...)  )
//最终执行的就是linux底层的一条汇编指令  lock cmpxchg 指令  硬件支持(锁定北桥信号)
 
//synchronized 和 volatile 最终底层都是使用 lock cmpxchg 指令

ABA问题

线程1读取了数据0做++,回来校验原值时,这个过程中,其他线程对数据0做了修改,只是最终结果又变成了0,即原值为A,过程中被改为了B,最终又被改为了A,就是ABA问题

/**
 * @Author blackcat
 * @version: 1.0
 * @description:ABA 问题
 */
@Slf4j
public class ABATest {

    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();   //
        sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }


    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();
        sleep(1);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }

    private static void sleep(int i) {
        try {
            Thread.sleep(i * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 [main] DEBUG com.blact.cat.first.AtomicTest - main start...
 [t1] DEBUG com.blact.cat.first.AtomicTest - change A->B true
 [t2] DEBUG com.blact.cat.first.AtomicTest - change B->A true
 [main] DEBUG com.blact.cat.first.AtomicTest - change A->C true

**解决办法:**数据0加上版本号,每个线程读取数据0时连同版本号一起读取, 每次处理完回来比较的时候连同版本号一起比较即可。 AtomicStampedReference 可带版本

/**
 * @Author blackcat
 * @create 2021/8/2 14:17
 * @version: 1.0
 * @description:
 */
@Slf4j
public class AtomicStampedReferenceTest {

    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();  
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();
        sleep(1);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }

    private static void sleep(int i) {
        try {
            Thread.sleep(i * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 [main] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - main start...
 [main] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - 版本 0
 [t1] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - change A->B true
 [t1] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - 更新版本为 1
 [t2] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - change B->A true
 [t2] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - 更新版本为 2
 [main] DEBUG com.blact.cat.first.AtomicStampedReferenceTest - change A->C false

原子更新基本类型

使用原子的方式更新基本类型,Atomic包提供了以下3个类。

AtomicBoolean:原子更新布尔类型。

AtomicInteger:原子更新整型。

AtomicLong:原子更新长整型。

/**
 * @Author blackcat
 * @create 2021/8/2 14:42
 * @version: 1.0
 * @description:AtomicInteger 相关方法
 */
@Slf4j
public class AtomicIntegerTest {

    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);
        // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        log.info("getAndIncrement:{}", i.getAndIncrement());
        // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        log.info("incrementAndGet:{}", i.incrementAndGet());
        // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
        log.info("decrementAndGet:{}", i.decrementAndGet());
        // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
        log.info("getAndDecrement:{}", i.getAndDecrement());
        // 获取并加值(i = 0, 结果 i = 2, 返回 0)
        log.info("getAndAdd:{}", i.getAndAdd(2));
        // 加值并获取(i = 2, 结果 i = 0, 返回 0)
        log.info("addAndGet:{}", i.addAndGet(-2));
        // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        log.info("getAndUpdate:{}", i.getAndUpdate(p -> p - 2));
        // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        log.info("updateAndGet:{}", i.updateAndGet(p -> p + 2));
        // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        log.info("getAndAccumulate:{}", i.getAndAccumulate(10, (p, x) -> p + x));
        // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        log.info("accumulateAndGet:{}", i.accumulateAndGet(-10, (p, x) -> p + x));
    }
}

原子更新数组类型

通过原子的方式更新数组里的某个元素,Atomic包提供了以下的类。

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

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

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

/**
 * @Author blackcat
 * @create 2021/8/2 19:42
 * @version: 1.0
 * @description:
 */
@Slf4j
public class AtomicIntegerArrayTest {

    static int[] value = new int[]{1, 2};

    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //以原子方式将数组位置i的元素设置成newValue值,则为3
        ai.getAndSet(0, 3);
        log.info("AtomicIntegerArray:{}", ai.get(0));
        //如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值,重新设为1
        boolean compareAndSet = ai.compareAndSet(0, 3, 1);
        log.info("{}-------{}", compareAndSet, ai.get(0));
    //int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。返回2
        ai.getAndAdd(0, 1);
        boolean addCompare = ai.compareAndSet(0, 3, 1);
        log.info("add after------{}-------{}", addCompare, ai.get(0));
        log.info("value;{}", value[0]);
    }
}

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

原子更新引用类型

AtomicReference:原子更新引用类型。

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

AtomicMarkableReference:原子更新带有标记位的引用类型。

**
 * @Author blackcat
 * @create 2021/8/2 21:08
 * @version: 1.0
 * @description:
 */
@Slf4j
public class AtomicReferenceTest {

    public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();

    public static void main(String[] args) {
        User user = new User("小黑", 15);
        atomicUserRef.set(user);
        log.info("before:{}",atomicUserRef.get().toString());
        User updateUser = new User("佩玲", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        log.info("after:{}",atomicUserRef.get().toString());
    }

    static class User {
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

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

原子字段更新器

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

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

AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起

来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的

ABA问题。

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

/**
 * @Author blackcat
 * @create 2021/8/2 21:15
 * @version: 1.0
 * @description:
 */
@Slf4j
public class AtomicIntegerFieldUpdaterTest {

    private volatile int field;

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterTest.class, "field");
        AtomicIntegerFieldUpdaterTest old = new AtomicIntegerFieldUpdaterTest();
        fieldUpdater.compareAndSet(old, 0, 10);
        // 修改成功 field = 10
        System.out.println(old.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(old, 10, 20);
        System.out.println(old.field);
        // 修改失败 field = 20
        fieldUpdater.compareAndSet(old, 10, 30);
        System.out.println(old.field);
    }
}

原子累加器

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * @Author blackcat
 * @create 2021/8/2 22:07
 * @version: 1.0
 * @description:累加器比较
 */
public class AccCompare {

    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>(10);
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 4; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            demo(() -> new LongAdder(), adder -> adder.increment());
        }
        for (int i = 0; i < 5; i++) {
            demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }
}
20000000 cost:59
20000000 cost:46
20000000 cost:51
20000000 cost:60
20000000 cost:45
20000000 cost:340
20000000 cost:563
20000000 cost:519
20000000 cost:527
20000000 cost:523

AtomicLong是多个线程针对单个热点值value进行原子操作。而LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。

比如有三个ThreadA、ThreadB、ThreadC,每个线程对value增加10。

对于AtomicLong,最终结果的计算始终是下面这个形式 value= 10+10+10=30

但是对于LongAdder来说,内部有一个base变量,一个Cell[]数组。 base变量:非竞态条件下,直接累加到该变量上 Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]

value = base + cell[0]+....+cell[i]

Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。

/**
 * @Author blackcat
 * @create 2021/8/2 21:24
 * @version: 1.0
 * @description:获取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);
        }
    }

    static Unsafe getUnsafe() {
        return unsafe;
    }
}

/**
 * @Author blackcat
 * @create 2021/8/2 21:25
 * @version: 1.0
 * @description:自己实现AtomicData
 */
@Slf4j
public class AtomicData {

    private volatile int data;
    static final Unsafe unsafe;
    static final long DATA_OFFSET;
    static {
        unsafe = UnsafeAccessor.getUnsafe();
        try {
            // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
            DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }
    public AtomicData(int data) {
        this.data = data;
    }

    public void decrease(int amount) {
        int oldValue;
        while(true) {
            // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
            oldValue = data;
            // cas 尝试修改 data 为 旧值 - amount,如果期间旧值被别的线程改了,返回 false
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
                return;
            }
        }
    }

    public void increment(int amount) {
        int oldValue;
        while(true) {
            oldValue = data;
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + amount)) {
                return;
            }
        }
    }
    public int getData() {
        return data;
    }


    public static void main(String[] args) throws InterruptedException {
        AtomicData atomicData = new AtomicData(100);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                atomicData.decrease(1);
            }
        }, "add");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                atomicData.increment(1);
            }
        }, "sub");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("result:{}", atomicData.getData());
    }
}