JUC-无锁

20

一、问题引出

1.1 需求与问题

需求:模拟一千个线程进行取款操作,需要保证取款方法的线程安全。

  • 代码示例(线程不安全)
public interface Account {

    /**
     * 获取余额。
     *
     * @return {@link Integer}
     */

    Integer getBalance();

    /**
     * 取款。
     *
     * @param amount 数额
     */

    void withdraw(Integer amount);

    /**
     * 模拟一千个用户取款的方法:
     * 方法内会启动 1000 个线程,每个线程做 -1 元 的操作;
     * 如果初始余额为 1000 那么正确的结果应当是 0。
     *
     * @param account 账户
     * @throws InterruptedException 中断异常
     */

    static void oneThousandUsersWithdraw(Account account) throws InterruptedException {

        List<Thread> threads = new ArrayList<>();

        Instant start = Instant.now();

        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> account.withdraw(1)));
        }

        threads.forEach(Thread::start);

        for (Thread thread : threads) {
            thread.join();
        }

        Instant end = Instant.now();
        long costTime = Duration.between(start, end).toMillis();
        System.out.println("余额为:" + account.getBalance() + ",花费时间为:" + costTime + "ms。");
    }


    @AllArgsConstructor
    class AccountUnsafe implements Account {

        private Integer balance;

        @Override
        public Integer getBalance() {
            return balance;
        }

        @Override
        public void withdraw(Integer amount) {
            balance -= amount;
        }

        public static void main(String[] args) throws InterruptedException {
            Account.oneThousandUsersWithdraw(new AccountUnsafe(1000));
            // 第一次执行结果:余额为:7,花费时间为:86ms。
            // 第二次执行结果:余额为:3,花费时间为:89ms。
        }
    }
}

1.2 解决方式

  • 使用『锁』方式解决线程不安全问题
    @AllArgsConstructor
    class AccountSafeByLock implements Account {

        private Integer balance;

        @Override
        public synchronized Integer getBalance() {
            return balance;
        }

        @Override
        public synchronized void withdraw(Integer amount) {
            balance -= amount;
        }

        public static void main(String[] args) throws InterruptedException {
            Account.oneThousandUsersWithdraw(new AccountSafeByLock(1000));
            // 第一次执行结果:余额为:0,花费时间为:92ms。
            // 第二次执行结果:余额为:0,花费时间为:95ms。
        }
    }
  • 使用『无锁』方式解决线程不安全问题
    class AccountSafeByNoLock implements Account {

        private final AtomicInteger balance;

        public AccountSafeByNoLock(Integer balance) {
            this.balance = new AtomicInteger(balance);
        }

        @Override
        public Integer getBalance() {
            return this.balance.get();
        }

        @Override
        public void withdraw(Integer amount) {
            while (true) {
                int prev = getBalance();
                int next = prev - amount;
                if (this.balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }

        public static void main(String[] args) throws InterruptedException {
            Account.oneThousandUsersWithdraw(new AccountSafeByNoLock(1000));
            // 第一次执行结果:余额为:0,花费时间为:84ms。
            // 第二次执行结果:余额为:0,花费时间为:85ms。
        }
    }

二、CAS 与 volatile

2.1 CAS

上述使用到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

  • 代码说明
        @Override
        public void withdraw(Integer amount) {
            while (true) {
                // 1.假设此处取到值为 1000。
                int prev = getBalance();
                // 2.在 1000 基础上减 1 = 999。
                int next = prev - amount;
                /* *
                 * 3.compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值:
                 *  3.1.不一致了,next 作废,返回 false 表示失败。
                 *  比如,别的线程已经做了减法,当前值已经被减成了 999,那么本线程的这次 999 就作废了,进入 while 下次循环重试。
                 *
                 *  3.2.一致,以 next 设置为新值,返回 true 表示成功,并结束循环。
                 */
                if (this.balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
  • 其中的关键是 compareAndSet(),它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
  • 示意图

  • 注意
    • 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 cpu 和多核 cpu 下都能够保证『比较-交换』的原子性。
    • 在多核状态下,某个核执行到带 lock 的指令时,cpu 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

2.2 volatile

  • 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
  • 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
  • CAS 必须借助 volatile 才能读取到共享变量的最新值来实现『比较-交换』的效果。

2.3 为什么无锁效率高?

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
  • 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外 cpu 的支持,cpu 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

2.4 CAS 的特点

  • 结合 CASvolatile 可以实现无锁并发,适用于线程数少多核 cpu 的场景下。
  • CAS 是基于乐观锁的思想:不怕别的线程来修改共享变量,就算改了也没关系,大不了再进行重试。
  • synchronized 是基于悲观锁的思想:得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发:
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
    • 如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

三、原子整数

  • java.util.concurrent 并发包提供了:
    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
  • 此处以 AtomicInteger 为例:
AtomicInteger i = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());

// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());

// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());

// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());

// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));

// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));

// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));

// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));

// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));

// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x))

四、原子引用

4.1 AtomicReference

一个可以被原子化更新的对象引用,多线程操作下,不会使 AtomicReference 最终达到不一致的状态。

  • 代码示例
@Slf4j
public class AtomicReferenceSample {

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

    private static void otherThreadCAS() throws InterruptedException {
        new Thread(() -> {
            boolean isSuccess = ref.compareAndSet(ref.get(), "value 1");
            log.debug("t1 try set={}", isSuccess);
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            boolean isSuccess = ref.compareAndSet(ref.get(), "value 2");
            log.debug("t2 try set={}", isSuccess);
        }, "t2").start();
    }

    public static void main(String[] args) throws InterruptedException {

        otherThreadCAS();
        TimeUnit.SECONDS.sleep(1);
        boolean isSuccess = ref.compareAndSet(ref.get(), "value 3");
        log.debug("main try set={}", isSuccess);
        log.debug("last value={}", ref.get());

        // [t1] t1 try set=true
        // [t2] t2 try set=true
        // [main] main try set=true
        // last value=value 3
    }
}
  • 问题点:主线程仅能判断出共享变量的值与最初值是否相同,不能感知到值改变后又改回的中间情况。
  • 只要有其它线程动过了共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个标记,此时推荐使用 AtomicStampedReference

4.2 AtomicStampedReference

一个带标记的原子化更新对象引用。

  • 代码示例
@Slf4j
public class AtomicStampedReferenceSample {

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

    private static void otherThreadCAS() throws InterruptedException {

        new Thread(() -> {
            log.debug("t1 try set={}", ref.compareAndSet(ref.getReference(), "value 1",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("t1 stamp={}", ref.getStamp());
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            log.debug("t2 try set={}", ref.compareAndSet(ref.getReference(), "initValue",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("t2 stamp={}", ref.getStamp());
        }, "t2").start();
    }


    public static void main(String[] args) throws InterruptedException {
        // 获取值。
        String prev = ref.getReference();
        // 获取版本号。
        int stamp = ref.getStamp();
        log.debug("main stamp={}", stamp);
        // 模拟中间有其它线程干扰。
        otherThreadCAS();
        TimeUnit.SECONDS.sleep(1);
        // 尝试改为 value 3
        log.debug("main try set={}", ref.compareAndSet(prev, "value 3", stamp, stamp + 1));

        // [main] main stamp=0
        // [t1] t1 try set=true
        // [t1] t1 stamp=1
        // [t2] t2 try set=true
        // [t2] t2 stamp=2
        // [main] main try set=false
    }
}
  • AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程。但有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,此时就推荐使用 AtomicMarkableReference

4.3 AtomicMarkableReference

维护一个对象引用以及一个整数 "stamp",它可以被原子化地更新。

  • 代码示例
@Slf4j
public class AtomicMarkableReferenceSample {

    private static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("initValue", true);

    public static void main(String[] args) throws InterruptedException {

        // 主线程获取值。
        String prev = ref.getReference();

        log.debug("main get ref={}", prev);

        // 其他线程改变了引用变量。
        new Thread(() -> {
            ref.set("value 1", false);
            log.debug("t1 set value");
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        // 主线程尝试设置值。
        boolean isSuccess = ref.compareAndSet(prev, "value 2", true, false);
        log.debug("main try set={}", isSuccess);

        // [main] main get ref=initValue
        // [t1] t1 set value
        // [main] main try set=false
    }
}

五、原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
  • 此处以 AtomicIntegerArray 为例:
@Slf4j
public class AtomicIntegerArraySample {

    /**
     * 示例一:使用不安全的整型数组。
     *
     * @throws InterruptedException 中断异常
     */
    
    @Test
    public void unSafeArray() throws InterruptedException {

        // 创建一个长度为 2 的数组。
        Integer[] ints = new Integer[2];
        List<Thread> threads = new ArrayList<>();
        // 3个线程对它执行操作。
        for (int i = 0; i < 3; i++) {
            // 每个线程对数组做1次操作 。
            int finalI = i;
            threads.add(new Thread(() -> {
                for (int j = 0; j <= 1; j++) {
                    ints[finalI] = j;
                }
            }, "t_" + i));
        }

        // 启动线程,并等待所有线程执行结束。
        threads.forEach(Thread::start);
        for (Thread thread : threads) {
            thread.join();
        }

        log.debug("array={}", Arrays.toString(ints));
        // Exception in thread "t_2" java.lang.ArrayIndexOutOfBoundsException
        // [main] array=[1, 1]
    }


    /**
     * 示例二:使用原子的整型数组。
     *
     * @throws InterruptedException 中断异常
     */

    @Test
    public void safeArray() throws InterruptedException {

        // 创建一个长度为 2 的『原子数组』。
        AtomicIntegerArray ints = new AtomicIntegerArray(2);
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j <= 1; j++) {
                    ints.getAndIncrement(j);
                }
            }, "t_" + i));
        }

        threads.forEach(Thread::start);
        for (Thread thread : threads) {
            thread.join();
        }

        log.debug("array={}", ints);
        // [main] array=[3, 3]
    }
}

六、字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • 注意:使用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
  • 此处以 AtomicIntegerFieldUpdater 为例:
@Slf4j
public class AtomicIntegerFieldUpdaterSample {

    /* *
     * 注意:要使用字段更新器必须使用 volatile 进行修饰。
     */

    private volatile int field = 0;

    public static void main(String[] args) {

        AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterSample> aifu =
                AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterSample.class, "field");

        AtomicIntegerFieldUpdaterSample sample = new AtomicIntegerFieldUpdaterSample();

        // 将 field 字段值更新为 1。
        aifu.compareAndSet(sample, 0, 1);
        log.debug("updated field ={}", sample.field);
        // updated field =1

        // 将 field 字段值更新为 2。
        aifu.compareAndSet(sample, 1, 2);
        log.debug("updated field ={}", sample.field);
        // updated field =2

        // 期望值不存在,则修改失败。
        aifu.compareAndSet(sample, 1, 3);
        log.debug("updated field ={}", sample.field);
        // updated field =2
    }
}

七、原子累加器性能比较

比较 AtomicLongLongAdder 性能差异。

  • 代码示例
@Slf4j
public class AtomicLongAndLongAdderComparePerfSample {

    /**
     * 用于累加器比较性能的方法。
     *
     * @param sup    供给型接口 - 获取累加器。
     * @param action 消费型接口 - 接收值。
     * @throws InterruptedException 中断异常
     */

    private static <T> void comparePerf(Supplier<T> sup, Consumer<T> action) throws InterruptedException {
        T adder = sup.get();
        Instant start = Instant.now();
        List<Thread> ts = new ArrayList<>();
        // 40 个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 50_0000; j++) {
                    action.accept(adder);
                }
            }));
        }

        ts.forEach(Thread::start);

        for (Thread t : ts) {
            t.join();
        }

        Instant end = Instant.now();
        long cost = Duration.between(start, end).toMillis();
        log.debug("total={},cost={}ms", adder, cost);
    }

    public static void main(String[] args) throws InterruptedException {

        // 两种累加器,各输出3次的结果。
        for (int i = 0; i < 3; i++) {
            comparePerf(LongAdder::new, LongAdder::increment);
        }
        // total=20000000,cost=79ms
        // total=20000000,cost=10ms
        // total=20000000,cost=9ms

        log.debug("------------------------------------------------");

        for (int i = 0; i < 3; i++) {
            comparePerf(AtomicLong::new, AtomicLong::getAndIncrement);
        }
        // total=20000000,cost=271ms
        // total=20000000,cost=276ms
        // total=20000000,cost=202ms
    }
}
  • 结论LongAdder 性能优于 AtomicLong
  • LongAdder 实现思路:性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 cas 重试失败,从而提高性能

八、Unsafe

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

8.1 CAS 操作

  • 注意事项(这里 Unsafe 名字的 “不安全” 并不是指"线程不安全",而是指操作底层的不安全性,具体原因如下)

    • 不受 JVM 管理,也就意味着无法被 GC,需要我们手动 GC稍有不慎就会出现内存泄漏
    • Unsafe 的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是 JVM 崩溃级别的异常,会导致整个 JVM 实例崩溃,表现为应用程序直接崩掉
    • 直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
  • 代码示例

public class UnsafeAccessor {

    private static final 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;
    }

}

@Slf4j
@Data
class Student {

    private volatile int id;
    private volatile String name;

    public static void main(String[] args) throws NoSuchFieldException {

        Unsafe unsafe = UnsafeAccessor.getUnsafe();
        Field id = Student.class.getDeclaredField("id");
        Field name = Student.class.getDeclaredField("name");
        // 获得成员变量的偏移量。
        long idOffset = unsafe.objectFieldOffset(id);
        long nameOffset = unsafe.objectFieldOffset(name);
        Student student = new Student();
        // 使用 cas 方法替换成员变量的值。
        unsafe.compareAndSwapInt(student, idOffset, 0, 20);
        unsafe.compareAndSwapObject(student, nameOffset, null, "张三");

        log.debug("student={}", student);
        // student=Student(id=20, name=张三)
    }
}

8.2 解决最开始提出的问题

需求:通过 UnsafeCAS 方式,编写自定义的 AtomicData 类并实现 Account 接口,最终完成模拟一千个取款的线程安全操作。

  • 代码示例
public class AtomicData {

    /**
     * 变量 - 使用 volatile 修饰。
     */
    private volatile int data;

    /**
     * 操作底层的实例。
     */

    private static final Unsafe UNSAFE = UnsafeAccessor.getUnsafe();

    /**
     * 偏移量。
     */

    private static final long DATA_OFFSET;

    static {

        try {
            Field field = AtomicData.class.getDeclaredField("data");
            // 偏移量,用于 Unsafe 直接访问该属性。
            DATA_OFFSET = UNSAFE.objectFieldOffset(field);
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }

    }

    public AtomicData(int data) {
        this.data = data;
    }

    public int getData() {
        return this.data;
    }

    /**
     * CAS 自减方法。
     *
     * @param amount 数量。
     */

    public void decrease(int amount) {
        while (true) {
            int oldValue = data;
            int newValue = oldValue - amount;
            // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false。
            if (UNSAFE.compareAndSwapInt(this, DATA_OFFSET, oldValue, newValue)) {
                return;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 使用自定义实现的 AtomicData 完成原子操作。
        AtomicData atomicData = new AtomicData(1000);

        Account.oneThousandUsersWithdraw(new Account() {
            @Override
            public Integer getBalance() {
                return atomicData.getData();
            }

            @Override
            public void withdraw(Integer amount) {
                atomicData.decrease(amount);
            }
        });

        // 第一次运行:balance:0,cost:50ms
        // 第二次运行:balance:0,cost:49ms
    }
}
  • 总结:可以看到使用 Unsafe 直接操作底层的效率更高

九、结束语

“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶