JUC P3 共享模型之无锁同步,CAS,原子类,Unsafe类 基础+代码

JUC P3 共享模型之无锁同步,CAS,原子类,Unsafe类 基础+代码

教程:https://www.bilibili.com/video/BV16J411h7Rd

7. 共享模型之无锁

  • CASvolatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe

7.1 取款问题

创建 1000 个线程修改余额。

7.1.1 上锁解决方案

public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountUnsafe(10000);
        Account.demo(account);
    }
}

class AccountUnsafe implements Account {
    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

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

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

interface Account {
    /**
     * @return 查看余额
     */
    Integer getBalance();

    /**
     * @param amount 取款
     */
    void withdraw(Integer amount);

    /**
     * @param account 账户
     *                创建 1000 个线程取款
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();

        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("最终余额: " + account.getBalance() + " cost: " + TimeUnit.NANOSECONDS.toMillis((System.nanoTime() - start)) + "ms");
    }
}

在这里插入图片描述

7.1.2 无锁解决方案

public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountCAS(10000);
        Account.demo(account);
    }
}

class AccountCAS implements Account {
    private AtomicInteger balance;

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

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

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取余额的最新值
            int latest = balance.get();
            // 修改余额
            int next = latest - amount;
            // 同步余额
            if (balance.compareAndSet(latest, next)) {
                break;
            }
        }
    }
}

interface Account {
    /**
     * @return 查看余额
     */
    Integer getBalance();

    /**
     * @param amount 取款
     */
    void withdraw(Integer amount);

    /**
     * @param account 账户
     *                创建 1000 个线程取款
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();

        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("最终余额: " + account.getBalance() + " cost: " + TimeUnit.NANOSECONDS.toMillis((System.nanoTime() - start)) + "ms");
    }
}

在这里插入图片描述

稍微快一丢丢。

7.2 CAS 与 volatile

AtomicInteger 的解决方案中没有使用锁来保护共享变量的线程安全,那么它是怎么实现的线程安全?

public void withdraw(Integer amount) {
	while (true) {
		int latest = balance.get();
		int next = latest - amount;
		if (balance.compareAndSet(latest, next)) {
		    break;
		}
	}
}

关键在于 compareAndSet,简称 CAS(Compare And Swap),它必须是原子操作。

AtomicInteger 源码中使用 volatile 关键字修饰变量保证变量可见性:
在这里插入图片描述

Note:
CAS 的原子性是由操作系统保证的,操作系统中有 CAS 原语

为什么无锁效率高

  • 无锁状态下,即使重试失败,线程始终在运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候发生上下文切换,进入阻塞。

Note:
打个比喻:无锁状态下相当于线程都在高速上,一旦发生上下文切换,就好比赛车要减速,等待唤醒又要重新打火,启动加速,代价比较大。

  • 无锁状态下,线程要保持运行需要得到 CPU 支持,虽然不会进入阻塞,但是会因为没有得到时间片,一直处于就绪状态,还是会导致上下文切换

CAS 特点

结合 CASvolatile 可以实现无锁并发,适用于线程数较少、多核 CPU 的情景下。

  • CAS 是基于乐观锁的思想:不怕别的线程修改共享变量,若其他线程进行修改,当前线程会再重新尝试修改一下
  • synchronized 是基于悲观锁的思想:防止别的线程来修改共享变量,当前线程上了锁,其他线程无法进入

CAS 体现的是无锁并发无阻塞并发

  • 因为没有 synchronized,因此线程不会陷入阻塞,这是效率提升的因素之一
  • 但是如果竞争激烈,重试会频繁发生,反而效率会受到影响

7.3 原子整数

在这里插入图片描述

7.3.1 常用方法

AtomicInteger i = new AtomicInteger(0); // 默认就是 0
i.compareAndSet(0, 1); // 先看是不是 0, 若是 0, 则修改为 1
log.debug("compareAndSet(0, 1) 之后的值: {}", i);

log.debug("自增并获取值: {}", i.incrementAndGet()); // ++i
log.debug("自减并获取值: {}", i.decrementAndGet()); // --i

log.debug("先获取后自增: {}", i.getAndIncrement()); // i++
log.debug("先获取后自减: {}", i.getAndDecrement()); // i--

log.debug("先加再取值: {}", i.addAndGet(5));
log.debug("先取值再加: {}", i.getAndAdd(5));

在这里插入图片描述

简化取款操作
public void withdraw(Integer amount) {
	/*
	while (true) {
		int latest = balance.get();
		int next = latest - amount;
		if (balance.compareAndSet(latest, next)) {
		    break;
		}
	}
	*/
	balance.getAndAdd(-1 * amount);
}

7.3.2 自定义修改

AtomicInteger i = new AtomicInteger(0); // 默认就是 0
log.debug("自定义先修改, 然后返回: {}", i.updateAndGet(x -> x - 50));
log.debug("自定义先返回, 然后修改: {}", i.getAndUpdate(x -> x * 20));
log.debug("查看当前值: {}", i);

在这里插入图片描述
仿照着实现 updateAndGet 功能:

public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger i = new AtomicInteger(5); // 默认就是 0
        
//        log.debug("自定义先修改, 然后返回: {}", i.updateAndGet(x -> x * 20));
        log.debug("自定义先修改, 然后返回: {}", updateAndGet(i, x -> x * 20));
        log.debug("当前值: {}", i);
    }

    public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
        int prev, next;
        do {
            prev = i.get();
            next = operator.applyAsInt(prev);
        } while (!i.compareAndSet(prev, next));
        return next;
    }
}

7.4 原子引用

在这里插入图片描述

7.4.1 AtomicReference 修改取款问题

public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountCAS(new BigDecimal("10000.0"));
        Account.demo(account);
    }
}

class AccountCAS implements Account {
    private final AtomicReference<BigDecimal> balance;

    public AccountCAS(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

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

    @Override
    public void withdraw(BigDecimal amount) {
        balance.updateAndGet(x -> x.subtract(amount));
    }
}

interface Account {
    BigDecimal getBalance();

    void withdraw(BigDecimal amount);

    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        long start = System.nanoTime();

        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("最终余额: " + account.getBalance() + " cost: " + TimeUnit.NANOSECONDS.toMillis((System.nanoTime() - start)) + "ms");
    }
}

在这里插入图片描述

7.4.2 ABA 问题

如果共享变量经过修改后又恢复到原来的值,当前线程不能发现别的线程对该变量的修改过程:

String prev = ref.get();

new Thread(() -> {
    log.debug("change A -> B: {} ?", ref.compareAndSet("A", "B"));
    log.debug("change B -> A: {} ?", ref.compareAndSet("B", "A"));
}, "t").start();

TimeUnit.SECONDS.sleep(1);
log.debug("change A -> C: {} ?", ref.compareAndSet(prev, "C"));

在这里插入图片描述

AtomicStampedReference 解决 ABA 问题

AtomicStampedReference 可以解决上述问题,当前线程如果发现别的线程对该变量进行修改,则自己的 CAS 操作失败。

通过设置一个版本号,可以追踪原子引用被修改了几次:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        String prev = ref.getReference();
        int stamp = ref.getStamp();
        log.debug("当前版本号: {}", stamp);

        new Thread(() -> {
            int version = ref.getStamp();
            // 四个参数分别是,旧值,新值,旧版本号,新版本号
            log.debug("change A -> B: {} ?", ref.compareAndSet("A", "B", version, version + 1));
            log.debug("change B -> A: {} ?", ref.compareAndSet("B", "A", version + 1, version + 2));
        }, "t").start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("当前版本号: {}", ref.getStamp());
        log.debug("change A -> C: {} ?", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
}

在这里插入图片描述

AtomicMarkableReference 解决 ABA 问题

有时候我们只关心变量是否被修改并不关心被修改了多少次,就可以使用 AtomicMarkableReference 来解决。

@Slf4j(topic = "c.InitTest")
public class InitTest {
    static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", true);

    public static void main(String[] args) throws InterruptedException {
        String prev = ref.getReference();

        new Thread(() -> {
        	// 四个参数分别是,旧值,新值,旧标记,新标记
            log.debug("change A -> B ?: {}", ref.compareAndSet("A", "B", true, false));
            log.debug("change B -> A ?: {}", ref.compareAndSet("B", "A", false, false));
        }, "t").start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("change A -> C ?: {}", ref.compareAndSet(prev, "C", true, false));
    }
}

在这里插入图片描述

7.5 原子数组

在这里插入图片描述

7.5.1 AtomicIntegerArray 配合 Lambda

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        demo(
                () -> new int[10],
                array -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );

        demo(
                () -> new AtomicIntegerArray(10),
                array -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                array -> System.out.println(array)
        );

    }

    /**
     * @param arraySupplier 无中生有, 没有参数 -> 结果
     * @param lengthFun     一个参数, 一个结果, x -> 结果 ---- PS: BiFunction: 两个参数, 一个结果 (x, y) -> 结果
     * @param putConsumer   一个参数没结果 void
     * @param printConsumer 两个参数没结果 void
     * @param <T>           泛型
     */
    private static <T> void demo(Supplier<T> arraySupplier,
                                 Function<T, Integer> lengthFun,
                                 BiConsumer<T, Integer> putConsumer,
                                 Consumer<T> printConsumer) {
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get();
        Integer length = lengthFun.apply(array);

        // 新建 length 个线程, 每个线程执行任务: 每个下标位置递增 10000 次
        for (int i = 0; i < length; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }

        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        // 打印线程
        printConsumer.accept(array);
    }
}

在这里插入图片描述

7.6 字段更新器

在这里插入图片描述

可以用来保护对象中属性赋值的原子性。

7.6.1 AtomicReferenceFieldUpdater

要求原子更新操作的属性必须声明为 volatile 保证可见性,否则会抛出异常 IllegalArgumentException: Must be volatile type

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Student student = new Student();
        // 参数: 类,属性类型,属性名称
        AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        log.debug(student.toString());
        log.debug("{}", updater.compareAndSet(student, null, "张三"));
        log.debug(student.toString());
    }
}

class Student {
    volatile String name;

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

在这里插入图片描述

7.7 原子累加器

为什么要使用原子累加器?原子类不已经有了 incrementAndGet 方法了吗?

  • 从性能角度,原子累加器比普通的原子类的自增方法性能更高

7.7.1 LongAdder 和 AtomicLong 比较

LongAdder 累加器在有竞争的时候设置多个累加单元,Thread-0 累加 Cell[0],Thread-1 累加 Cell[1]…最后将结果汇总。累加时操作不同的 Cell 变量,减少了 CAS 重试失败,从而提高性能。

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        demo(
                () -> new AtomicLong(0),
                adder ->  adder.getAndIncrement()
        );

        demo(
                () -> new LongAdder(),
                adder -> adder.increment()
        );
    }
    private static <T> void demo (Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 50_0000; j++) {
                    action.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("总和: " + adder + " 花费时间: " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) + " ms");
    }
}

在这里插入图片描述

7.7.2 原理分析

/**
 * Table of cells. When non-null, size is a power of 2.
 */
transient volatile Cell[] cells; // 累加单元数组,懒初始化
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base; // 没有竞争时,用 CAS 累加
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
transient volatile int cellsBusy; // 在 cells 创建或者扩容时置为 1,表示加锁
CAS 锁原理
@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        LockCas lockCas = new LockCas();

        new Thread(() -> {
            log.debug("begin...");
            lockCas.lock();
            try {
                log.debug("lock...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lockCas.unlock();
                log.debug("unlock...");
            }
        }).start();

        new Thread(() -> {
            log.debug("begin...");
            lockCas.lock();
            try {
                log.debug("lock...");
            } finally {
                lockCas.unlock();
                log.debug("unlock...");
            }
        }).start();
    }
}

class LockCas {
    private AtomicInteger state = new AtomicInteger(0);

    public void lock() {
        while (true) {
            if (state.compareAndSet(0, 1)) {
                break;
            }
        }
    }

    public void unlock() {
        state.set(0);
    }
}

在这里插入图片描述

Cell 类

前情提要:

  • CPU 与内存之间速度差异很大,需要靠预读数据至 Cache 提升效率。
  • Cache 地单位是缓存行,每个缓存行对应一块内存,一般是 64Byte
  • Cache 的加入会造成数据副本产生,即同一份数据会缓存在不同核心地缓存行中,
  • CPU 要保证数据一致性,若某个 CPU 核心更改了数据,其他 CPU 核心对应的整个缓存行必须失败

伪共享:因为 Cell 是数组形式的,在内存中是连续存储的,一个 Cell 为 24 字节(16 Byte 对象头 + 8 Byte value 属性),因此缓存行中可以存储 2 个 Cell 对象,这样就可能导致两个核心更新自己的 Cell 的时候让另外一个核心的缓存失效,需要重新读取内存。

在这里插入图片描述

@jdk.internal.vm.annotation.Contended 注解的作用:

  • 在此注解的对象或者字段前后增加 128 Byte 大小的 Padding,让 CPU 将对象预读到不同的缓存行,这样就不会造成对象缓存行的失效。
    在这里插入图片描述

7.8 Unsafe

7.8.1 概述

Unsafe 对象提供了底层操作内存和线程的方法。

Note:
了解更多参考:
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
https://www.cnblogs.com/throwable/p/9139947.html

7.8.2 使用 Unsafe 类实现 CAS 修改属性

Unsafe 不能通过 new 或者 Unsafe.getUnsafe 获取对象,只允许启动类加载器加载的类才可以使用。

但是可以通过反射获取,使用 CAS 方法修改属性值:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 0. 通过反射获取 unsafe 对象
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        // 1. 获取域的偏移地址
        long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));

        Student student = new Student();

        // 2. 执行 CAS 操作
        unsafe.compareAndSwapInt(student, idOffset, 0, 100);
        unsafe.compareAndSwapObject(student, nameOffset, null, "王二麻子");

        log.debug(student.toString());
    }
}

@Data
class Student {
    volatile int id;
    volatile String name;
}

7.8.3 模拟实现原子整数类

参照 Atomic 源码实现,并测试:

public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountCAS(10000);
        Account.demo(account);
    }
}

class AccountCAS implements Account {
    private MyAtomicInteger balance;

    public AccountCAS(Integer balance) {
        this.balance = new MyAtomicInteger(balance);
    }

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

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

interface Account {
    /**
     * @return 查看余额
     */
    Integer getBalance();

    /**
     * @param amount 取款
     */
    void withdraw(Integer amount);

    /**
     * @param account 账户
     *                创建 1000 个线程取款
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();

        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("最终余额: " + account.getBalance() + " cost: " + TimeUnit.NANOSECONDS.toMillis((System.nanoTime() - start)) + "ms");
    }
}

/**
 * 模拟 AtomicInteger 的实现
 */
class MyAtomicInteger {
    private volatile int value;
    private static final long valueOffset;
    private static final Unsafe UNSAFE;

    static {
        UNSAFE = UnsafeAccessor.getUnsafe();
        try {
            valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

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

    public final int get() {
        return value;
    }

    public final int addAndGet(int amount) {
        int prev, next;
        do {
            // Unsafe 底层就是这么写的, 可以通过类对象和偏移量获取属性值
//            prev = UNSAFE.getIntVolatile(this, valueOffset);
            prev = value;
            next = prev + amount;
        } while (!UNSAFE.compareAndSwapInt(this, valueOffset, prev, next));
        return next;
//        return UNSAFE.getAndAddInt(this, valueOffset, amount) + amount;
    }
}

/**
 * 反射获取 Unsafe 对象工具类
 */
final class UnsafeAccessor {
    @SneakyThrows
    public static Unsafe getUnsafe() {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        ;
        theUnsafe.setAccessible(true);
        return (Unsafe) theUnsafe.get(null);
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇咔咔负负得正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值