java并发 day04CAS 原子整数 原子引用 原子数组 字段更新器和原子累加器 unsafe CPU缓存结构 不可变类 final的原理

Posted halulu.me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发 day04CAS 原子整数 原子引用 原子数组 字段更新器和原子累加器 unsafe CPU缓存结构 不可变类 final的原理相关的知识,希望对你有一定的参考价值。

CAS

compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
CAS必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
volatile修饰的共享变量,会保证每次读操作都会从主内存中获取最新值。在CAS操作中,最新值与CAS代码中的获取值不一致的时候,会重新获取最新值并再次比较。当一致的时候,就会进行数值的修改,以此来保证数据的安全性。

CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

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

1、没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
2、但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数(整数)

AtomicBoolean
AtomicInteger
AtomicLong

getAndIncrement() 类似于 i++
incrementAndGet() 类似于 ++i
decrementAndGet() 类似于–i
getAndDecrement() 类似于 i–
getAndAdd(5) 先获取i再加5
addAndGet(-5) 先加5再获取i
getAndUpdate(p -> p * 2) 进行乘除操作
.updateAndGet(p -> p * 2) 进行乘除操作

原子引用(小数、字符串。。)

AtomicReference
AtomicMarkableReference
AtomicStampedReference
AtomicReference<BigDecimal> bigdecimal = new AtomicReference<>(new BigDecimal("10.1"));
AtomicReference<String> str = new AtomicReference<>("A");

ABA问题

@Slf4j
public class Test36 {

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

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

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

在这里插入图片描述

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能察觉到这种从 A 改为 B 又 改回 A 的情况。

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

@Slf4j
public class Test37 {

    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();
        TimeUnit.SECONDS.sleep(1);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

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

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference,
boolean success = ref.compareAndSet(prev, next, true, false);

原子数组

AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
保护数组的元素

字段更新器和原子累加器

字段更新器

AtomicReferenceFieldUpdater // 域 字段
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

在这里插入图片描述

public class Test40 {

    public static void main(String[] args) {
        Student stu = new Student();

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

        System.out.println(updater.compareAndSet(stu, null, "张三"));
        System.out.println(stu);
    }
}

class Student {
    volatile String name;

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

原子累加器

LongAdder
LongAdder性能比AtomicLong好
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

unsafe

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

public class UnsafeTest {
    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的CAS操作

public class UnsafeTest {
    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);

        //1、获取域得偏移地址
        long idOffSet = unsafe.objectFieldOffset(T.class.getDeclaredField("id"));
        long nameOffSet = unsafe.objectFieldOffset(T.class.getDeclaredField("name"));
        //2、执行CAS操作
        T t = new T();
        unsafe.compareAndSwapInt(t,idOffSet,0,1);
        unsafe.compareAndSwapObject(t,nameOffSet,null,"张三");

        log.info("t:{}",t);
    }
}
class T{
    volatile int id ;
    volatile String name;

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

unsafe原子整数类

public class MyAtomicInteger {
    public static void main(String[] args) throws InterruptedException {
        MyAtomicInteger atomicInteger = new MyAtomicInteger(1000);
        new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                atomicInteger.increment(1);
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                atomicInteger.increment(1);
            }
        }).start();
        Thread.sleep(3000);
        int value = atomicInteger.getValue();
        log.info("value:{}",value);
    }
    private volatile int value;
    private static Unsafe unsafe;
    private static long valueOffset;

    static {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            // value 属性在 MyAtomicInteger 对象中的偏移量,用于 Unsafe 直接访问该属性
            valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    private int getValue() {
        return value;
    }

    private void increment(int amount) {
        while (true) {
            int pre = this.value;
            int next = pre + amount;
            if(unsafe.compareAndSwapInt(this,valueOffset,pre,next)){
                break;
            };
        }
    }

}

CPU缓存结构

在这里插入图片描述

在这里插入图片描述

1、因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
2、而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64
byte(8 个 long)
3、缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
4、CPU要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
5、@sun.misc.Contended 的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

不可变类

不可变类在多线程中也可以保证数据得安全性。比如Sting类、DateTimeFormatter类
SimpleDateFormat并不是线程安全的。在 Java 8 后,提供了一个新的日期格式化类DateTimeFormatter以保证线程安全。

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String format = formatter.format(LocalDate.now());
        System.out.println(format);
        TemporalAccessor parse = formatter.parse(format);
        System.out.println(parse);

不可变类的设计

使用final修饰类

  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

String类就是使用保护性拷贝来保证数据的不可变。
通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】(全新的对象)

final的原理

1、设置final的原理
在这里插入图片描述
在这里插入图片描述

final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0的情况。
final通过加入写屏障,来保证数据对其他线程的可见性,以及防止写屏障之前的指令放到写屏障之后。

2、获取final变量的原理
在这里插入图片描述

在这里插入图片描述

final修饰的成员变量在字节码指令中会直接压入栈中,并不会通过堆(并不会走getStatic指令)。

以上是关于java并发 day04CAS 原子整数 原子引用 原子数组 字段更新器和原子累加器 unsafe CPU缓存结构 不可变类 final的原理的主要内容,如果未能解决你的问题,请参考以下文章

java并发 day04CAS 原子整数 原子引用 原子数组 字段更新器和原子累加器 unsafe CPU缓存结构 不可变类 final的原理

java并发 day04CAS 原子整数 原子引用 原子数组 字段更新器和原子累加器 unsafe CPU缓存结构 不可变类 final的原理

Java并发多线程编程——原子类AtomicInteger的ABA问题及原子更新引用

并发编程(学习笔记-共享模型之无锁)-part5

java基础---多线程---JUC原子类

Java并发编程系列- 原子操作与CAS