并发编程系列学习笔记05(共享模型之无锁并发)

共享模型之无锁并发

问题案例:账户取款

  • 通过取款案例可知,多线程取款时对Integer读写操作存在线程安全性问题
  • 原因:可对字节码分析,多线程执行时,可能导致指令交替执行

解决方案

  • 有锁

    • 加synchronized,可保证多线程下共享变量的原子性,前面已经详细讲过
  • 无锁

    • 使用AtomicInteger原子整型类的CAS机制
    • 性能在线程竞争不激烈时,相比加锁性能高很多
  • 代码案例

public void withdraw(Integer amount) {
    while (true) {
        int prev = balance.get();
        int next = prev - amount;
        if (balance.compareAndSet(prev, next)) {
            break;
        }
    }
    // 可以简化为下面的方法
    // balance.addAndGet(-1 * amount);
}

CAS与volatile

  • cas需要volatile的支持,底层关键指令:lock cmpxchg(X86 架构),CPU执行此类指令将锁住总线,待执行完毕后解锁,过程中不会被线程调度机制打断,保证其原子性

  • 其元子类内部的value属性上存在volatile修饰

  • cas相比加锁,CPU高速运行,但没有上下文切换,无锁无阻塞

  • 类比高速跑道上的赛车,高速运行速度快,但一旦要停下来再跑,就需要经过减速、熄火、启动、加速等等过程,才会恢复高速,代价很大

  • 适合线程数较少,且多核CPU上性能发挥更佳,线程竞争激烈,重试过多,性能可能不佳

  • CAS属于乐观锁思想

    • 乐观:不怕别人来改,改了我就重试嘛
    • 悲观:得先烦着别人来改,我先上锁,你们都不要进来

原子整数

  • 包括

    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
  • 代码案例

# TODO

原子引用

  • AtomicReference(普通引用)
  • 存在ABA问题:一个共享变量被来回修改无法感知,一般情况下对业务没有影响
  • 解决方案1:AtomicStampedReference(比较值+version)
  • 解决方案2:AtomicMarkableReference(只关心是否被改了)
  • 案例:主人加保洁阿姨换垃圾袋

原子数组

  • 思考:普通数组,其内部元素是否安全?
  • 案例代码:lambda 实现多线程对数组元素进行累加,存在问题
  • 解决方案:AtomicIntegerArray
  • 代码案例
/**
 * 对比普通数组与原子数组在多线程下的安全性
 * @date 2021年8月1日
 * @author qinchen
 */
public class TestAtomicIntegerArray {

    public static void main(String[] args) {
        // 普通数组,不能保证数组中的元素的线程安全
        demo(
                // 创建一个长度为10的数组
                () -> new int[10],
                // 通过输入的类型参数(1已决定是数组),获取数组长度
                (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)
        );
    }

    private static <T> void demo(
            Supplier<T> arraySuppler,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer) {

        // 创建一个 list 存放创建的所有线程
        List<Thread> ts = new ArrayList<>();
        // 取到输入的对象
        T array = arraySuppler.get();
        // 将对象作为输入参数,返回一个Integer
        int length = lengthFun.apply(array);
        // 根据 length,遍历创建 length 个 thread
        for (int i = 0; i < length; i++) {
            ts.add(new Thread(() -> {
                // 每个线程中,对 putConsumer 做 10000 次运算
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }

        ts.forEach(Thread::start);

        // 等所有线程执行完毕
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 打印 10 个线程 最后对 数组元素进行累加后的结果
        printConsumer.accept(array);

    }
}

原子更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • 可原子更新某个对象的某个域(可以理解为字段),需配合volatile关键字使用
  • 代码案例
/**
 * 对象属性的原子更新器
 */
public class TestAtomicReferenceFieldUpdater {

    public static void main(String[] args) {

        Student student = new Student();

        AtomicReferenceFieldUpdater<Student, String> updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

        boolean b = updater.compareAndSet(student, null, "张三");
        if(b) {
            System.out.println(student);
        }

    }

}

class Student {

    /**
     * 注意:必须是 volatile 类型
     */
    volatile String name;

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

原子累加器

  • 特点

    • LongAdder相比AtomicLong的.getAndIncrement,其性能更高
    • 其原因主要因为内部设置了多个累加单元,各线程自行先累加,最后在汇总,总分总思想
    • 这样减少了CAS失败重试的概率,从而提升了性能
  • 代码案例

/**
 * 累加器,性能比使用原子类更好
 * 原理:线程有竞争时,设置多个【累加单元】,对应不同线程
 * 最后将多个【累加单元】进行汇总,这样累加操作不同的单元变量,减少了CAS重试失败频率
 * 从而提升了性能
 */
public class TestLongAdder {

    public static void main(String[] args) {

        // 想对较慢
        demo(() -> new AtomicLong(0),
                (addr) -> addr.getAndIncrement());

        // 性能更好
        demo(() -> new LongAdder(),
                (addr) -> addr.increment());
    }

    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {

        T t = adderSupplier.get();

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

        for (int i = 0; i < 4; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(t);
                }
            }));
        }

        long start = System.nanoTime();

        ts.forEach(Thread::start);
        ts.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long end = System.nanoTime();

        System.out.println(t + " cost = " + (end - start) / 1000_000 + "ms");

    }

}
  • 源码解析

    • 来自:Doug Lea ,设计非常精巧

    • 内部关键域

      • volatile Cell[] cells 累加单元
      • volatile long base 没有竞争,CAS用此域
      • volatile int cellsBusy 创建或扩容时的标记
    • 缓存行伪共享问题

      • @sun.misc.Contended 防止缓存行的伪共享
      • 涉及到CPU三级缓存与内存
      • 缓存以缓存行为单位,一般为64byte
      • 缓存会导致数据产生多份副本,一份数据可能缓存在多个核心的缓存行中
      • CPU要保证数据一致性,需在某个核心修改数据后,其他核对应的缓存行必须失效
      • @sun.misc.Contended标注的对象前后各家128字节大小的padding,目的是让其占用不同核心缓存行

Unsafe

  • 特点

    • 非常底层的操作内存线程的方法
    • 不能直接调用,需要进行反射调用
    • 名字提醒开发者一般不建议直接使用,误用容易出现一些对系统不安全因素
  • 代码案例

public class TestUnsafe {

    /**
     * Unsafe 必须通过反射方式获取到该类对象
     *
     * @param args
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");

        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        System.out.println(unsafe);

        // 下面通过 unsafe 修改对象属性,保证线程安全
        Teacher teacher = new Teacher();

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

        // 2. 执行 cas 操作
        unsafe.compareAndSwapInt(teacher, idOffset, 0, 1);
        unsafe.compareAndSwapObject(teacher, nameOffset, null, "张三");

        System.out.println(teacher);

    }
}

@Data
class Teacher {
    volatile int id;
    volatile String name;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值