Java并发编程:CAS概念、原子操作类(乐观锁思想)、LongAddr和AtomicLong、Unsafe

Java并发编程:CAS概念、原子操作类(乐观锁思想)、LongAddr和AtomicLong、Unsafe

共享模型之无锁

  • Java中 synchronizeReentranLock等独占锁就是 悲观锁思想的实现
  • 在Java中 java.util.concurretn.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
  • 管程即monitor阻塞式的悲观锁实现并发控制,这章我们将通过非阻塞式乐观锁的来实现并发控制

1、 问题提出

  • 有如下需求,保证account.withdraw取款方法的线程安全, 下面使用synchronized保证线程安全
@Slf4j
public class Test1 {
    public static void main(String[] args) {
        Account.demo(new AccountUnsafe(1000));
        Account.demo(new AccountCas(1000));

    }
}
class AccountUnsafe implements  Account{
    private Integer blance;

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

    @Override
    public Integer getBlance() {
        synchronized (this){
            return blance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        //通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
        synchronized (this){
            blance-=amount;
        }
    }
}


class AccountCas implements Account{
    //使用原子整数:底层使用CAS+重试的机制
    private AtomicInteger balance;

    public AccountCas(int blance){
        this.balance=new AtomicInteger(blance);
    }
    @Override
    public Integer getBlance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        while(true){
            //获得修改前的值
            int prev=balance.get();
            //获取修改后的值
            int next =prev-amount;
            //比较并设置值

            if(balance.compareAndSet(prev,next)){
                break;
            }
        }
    }
}


interface  Account{
    //获取余额
    Integer getBlance();

    //取款
    void withdraw(Integer amount);

    /***
     * JAva8之后接口新特性,可以添加默认方法
     * 方法内会启动1000个线程,每个线程做-10元的操作
     * 如果初始化月10000 那么正确的结果是0
     */

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

        ts.forEach(thread -> thread.start());

        ts.forEach(t ->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long end =System.nanoTime();
        System.out.println(account.getBlance()
        +   "cost:"+(end-start)/1000_000 +"ms");
    }
}

使用ReentranLock

class AccountReenLock implements  Account{
    ReentrantLock lock =new ReentrantLock();
    private Integer balance;
    public  AccountReenLock(int balance){
        this.balance=balance;
    }
    @Override
    public Integer getBlance() {
        return this.balance;
    }

    @Override
    public void withdraw(Integer amount) {
        lock.lock();
        try {
            balance-=amount;
        }finally {
            lock.unlock();
        }
    }
}

解决思路-无锁

  • 上面的代码使用synchronize加锁操作来保证线程安全但是synchronize加锁太消耗资源(因为底层使用了操作系统mutex指令,造成内核态和用户态的切换),这里我们使用无锁来解决此问题
class AccountCas implements Account{
    //使用原子整数:底层使用CAS+重试的机制
    private AtomicInteger balance;

    public AccountCas(int blance){
        this.balance=new AtomicInteger(blance);
    }
    @Override
    public Integer getBlance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        while(true){
            //获得修改前的值
            int prev=balance.get();
            //获取修改后的值
            int next =prev-amount;
            //比较并设置值

            if(balance.compareAndSet(prev,next)){
                break;
            }
        }
    }
}

结果如下:

0cost:670ms		//synchronize
0cost:562ms		//CAS自旋锁
0cost:644ms		//ReentranLock锁

CAS与volatile(重点

使用原子操作来保证线程访问共享资源的安全性,cas+重试的机制来确保(乐观锁思想),相对于悲观锁思想的synchronize,ReentranLock来说,cas的方式效率更好!

cas+重试的原理

  • 前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
@Override
public void withdraw(Integer amount) {
    // 核心代码
    // 需要不断尝试,直到成功为止
    while (true){
        // 比如拿到了旧值 1000
        int prev = balance.get();
        // 在这个基础上 1000-10 = 990
        int next = prev - amount;
        /*
         compareAndSet 保证操作共享变量安全性的操作:
         ① 线程A首先获取balance.get(),拿到当前的balance值prev
         ② 根据这个prev值 - amount值 = 修改后的值next
         ③ 调用compareAndSet方法, 首先会判断当初拿到的prev值,是否和现在的
         	balance值相同;
         	3.1、如果相同,表示其他线程没有修改balance的值, 此时就可以将next值
         		设置给balance属性
         	3.2、如果不相同,表示其他线程也修改了balance值, 此时就设置next值失败, 
				然后一直重试, 重新获取balance.get()的值,计算出next值,
				并判断本次的prev和balnce的值是否相同...重复上面操作
		*/
        if (atomicInteger.compareAndSet(prev,next)){
            break;
        }
    }
}

  • 其中的关键是compareAndSwap(比较并且设置值),它的简称就是CAS(也有Compare And Swap的说法),它必须是原子操作

1594776811158

流程:

  • 当一个线程要去修改Account对象中的值时,先获取值prev(调用get方法),然后在讲其设置为新的值next(调用cas方法)。在调用cas方法时,会将prevAccount中的余额进行比较。
    • 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
    • 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的next值(cas方法),直到修改成果为止。

注意:

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

在这里插入图片描述

volatile的作用

  • 在上面代码中的AtomicInteger类,**保存值的value属性使用了volatile修饰。**获取共享变量时,为了保证该变量的可见性,需要使用 volatile修饰。
  • volatile可以用来修饰 **成员变量和静态成员变量,**他可以避免线程从自己的工作缓存中查找变量的值。必须要到主存中获取它的值,线程操作volatile变量都是直接操作主存。 即一个线程对volatile变量的修改,对另一个线程可见。

注意:volatile仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能仅仅指令交错问题(不能保证原子性)

  • CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么CAS+重试(无锁)效率高

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

CAS的特点(乐观锁和悲观锁的特点)

结合CAS和volatile可以实现无锁并发,适用于线程数少、多核CPU的场景下。

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试呗。

  • synchronize是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想该,我改完了解开锁,你们才有机会。

  • CAS的体现是无锁并发、无阻赛并发,请仔细体会这两句话的意识

    • 因为没有使用synchronize,所以线程不会阻塞,这是效率提高的因素之一
    • 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响

原子整数(内部通过CAS来实现-AtomicInteger)

  • java.util.concurrent.atomic并发包提高了一些开发工具类,这里把它分为五类:
  • 使用原子的方式(共享数据为基本数据类型的原子类
    • AtomicInteger:整形原子量
    • AtomicLong:长整型原子类
    • AtomicBoolean:布尔型原子类
  • 上面三个类提供的方法几乎相同,所以我们将从AtomicInteger为例子来介绍先讨论原子整数类,以AtomicInteger为例讨论它的api接口:通过观察源码可以发现
  • AtomicInteger内部都是通过cas原理来实现的
package com.atomic;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/13 18:28
 */
public class Test2 {
    public static void main(String[] args) {
        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());

        //获取并之间()
        System.out.println(i.getAndDecrement());

        //获取并加值
        System.out.println(i.getAndAdd(5));


        System.out.println(i.addAndGet(-5));

        System.out.println(i.getAndUpdate(p ->p-2));

        System.out.println(i.updateAndGet(p ->p+2));

        System.out.println(i.getAndAccumulate(10,(p,x)->p+x));

        System.out.println(i.accumulateAndGet(-10,(p,x)->p+x));
    }
}

举个例子: updateAndGet的实现

package com.atomic;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntUnaryOperator;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/13 21:00
 */
public class Test3 {
    public static void main(String[] args) {
        AtomicInteger i =new AtomicInteger(5);

        updateAndGet(i,(operand -> {
            System.out.println(operand/2);
            return operand/2;
        }));

    }

    public static void updateAndGet(AtomicInteger i, IntUnaryOperator operator){
        while(true){
            int prev=i.get();   //5
            int next =operator.applyAsInt(prev);
            if(i.compareAndSet(prev,next)){
                break;
            }
        }
    }
}

步骤:

  • 调用updateAndGet方法,将共享变量i,IntUnaryOperator对象传递过去
  • updateAndGet方法内部,传过来的operator对象,调用IntUnaryOperator中的applyAsInt方法,实际调用的就是传递过来的对象的方法,进行/操作

在这里插入图片描述

原子引用(AtomicRefe)

原子引用的作用:保证应用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)

  • 为什么需要原子引用类型?(引用数据类型原子类)
    • AtomicReference
    • AtomicMarkableReference
    • AtomicStamedReference可以解决ABA问题

为什么需要原子引用类型保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人。)

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。

  • **AtomicReference:**引用类型原子类
  • **AtomicStampedReference:**原子更新带有 版本号的引用类型。该类将整数值与应用类型关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAs进行原子更新时可能出现的ABA问题。
  • AtomicMarkableReference :原子更新带有标记的引用类型。该类将boolean标记与引用关联起来~~,也可以解决使用CAS进行原子更新可能出现的ABA问题。~~

例子:使用原子引用实现BigDecimal存储款的线程安全:

下面这个是不安全的实现过程:

public class DecimalAccountUnsafe implements  DecimalAccount{
    BigDecimal balance;
    @Override
    public BigDecimal getBalance() {
        return balance;
    }

    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal balance =this.getBalance();
        this.balance=balance.subtract(amount);
    }
}

解决代码如下:在AtomicReference类中,存在一个value类型的变量,保存对BigDecimal对象的引用。

package com.atomic;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/13 21:15
 */
public class Test4 {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
        DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000")));

    }
}
class DecimalAccountCas implements DecimalAccount{

    //原子引用,泛型类型为小数类型
    private final AtomicReference<BigDecimal> balance;

    DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference(balance);
    }

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

    @Override
    public void withdraw(BigDecimal amount) {
        while (true){
            BigDecimal prev =balance.get();
            BigDecimal next = prev.subtract(amount);
            if(balance.compareAndSet(prev, next)){
                break;
            }
        }

    }
}
结果如下:
0
210

ABA问题及解决(重点

  • 如下程序所示,虽然 在other方法中存在两个线程对共享变量进行了修改,但是修改之后又变成了原值,main线程对修改过共享变量的过程是不可见的,这种操作这对业务代码并无影响。
public class Test1 {
static AtomicReference<String> ref = new AtomicReference<>("A");

public static void main(String[] args) {
    new Thread(() -> {
        String pre = ref.get();
        System.out.println("change");
        try {
            other();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Sleeper.sleep(1);
        //把ref中的A改为C
        System.out.println("change A->C " + ref.compareAndSet(pre, "C"));
    }).start();
}

static void other() throws InterruptedException {
    new Thread(() -> {
    	// 此时ref.get()为A,此时共享变量ref也是A,没有被改过, 此时CAS
    	// 可以修改成功, B
        System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
    }).start();
    Thread.sleep(500);
    new Thread(() -> {
    	// 同上, 修改为A
        System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
    }).start();
}
}

在这里插入图片描述

AtomicMarkableReference(标记cas的共享变量是否修改过)

  • AtomicMarkableReference可以给原子引用加上版本号追踪原子引用整个的变化过程,如:A->B->A->c,通过AtomicMarkableReference,我们可以知道,引用变量中途被更改了几次。
  • 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否被更改过,所以就有了AtomicMarkableReference

1594803309714

@Slf4j
public class TestABAAtomicMarkableReference {
    @SneakyThrows
    public static void main(String[] args) {
        GarbageBag bag =new GarbageBag("装满了垃圾");
        //参数2 mark 可以看做一个标记,表示垃圾装满了
        AtomicMarkableReference<GarbageBag> ref=new AtomicMarkableReference<>(bag,true);
        log.debug("主线程 start....");

        GarbageBag prev =ref.getReference();
        log.debug(prev.toString());

        new Thread(()->{
            log.debug("打扫卫生的线程 start。。。");
            bag.setDesc("空垃圾袋");
            while(!ref.compareAndSet(bag,bag,true,false)){

            }
            log.debug(bag.toString());
        }).start();
        Thread.sleep(1000);
        log.debug("主线程向换一只新的垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?"+success);
        log.debug(ref.getReference().toString());
    }
}
class GarbageBag{
    String desc;

    public GarbageBag(String desc){
        this.desc=desc;
    }
    public void setDesc(String desc){
        this.desc=desc;
    }

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

运行结果如下

23:00:24.062 guizy.TestABAAtomicMarkableReference [main] - 主线程 start...
        23:00:24.069 guizy.TestABAAtomicMarkableReference [main] - com.guizy.cas.GarbageBag@2be94b0f 装满了垃圾
        23:00:24.312 guizy.TestABAAtomicMarkableReference [Thread-0] - 打扫卫生的线程 start...
        23:00:24.313 guizy.TestABAAtomicMarkableReference [Thread-0] - com.guizy.cas.GarbageBag@2be94b0f 空垃圾袋
        23:00:25.313 guizy.TestABAAtomicMarkableReference [main] - 主线程想换一只新垃圾袋?
        23:00:25.314 guizy.TestABAAtomicMarkableReference [main] - 换了么?false
        23:00:25.314 guizy.TestABAAtomicMarkableReference [main] - com.guizy.cas.GarbageBag@2be94b0f 空垃圾袋

举例2

package com.atomic;

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/14 11:34
 */
public class TestAtomicMarkableReferenceABA2 {
    //指定版本号
    static AtomicMarkableReference<String> ref =new AtomicMarkableReference<>("A",true);

    public static void main(String[] args) {
        new Thread(()->{
           String pre= ref.getReference();
            System.out.println("change");

            other();

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("change A->C mark"+ref
            .compareAndSet(pre,"C",true
            ,false));
        }).start();
    }

    private static void other() {
        new Thread(()->{
            System.out.println("change A-> A mark"+ref
            .compareAndSet(ref.getReference(),"A",true,false));
        }).start();

    }

}

在这里插入图片描述

AtomicStampedReference和AtomicMarkableReference两的却比

  • AtomicStampedReference需要我们传入整形变量作为版本号,来判定是否更改过
  • AtomicMarkableReference需要我们传入布尔变量作为标记,来判定是否更改过

原子数组(AtomicIntegerArray)

  • 保证数组内的元素的线程安全
  • 使用原子的方式更新数组里面的某个元素
    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray:引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。实例代码

  • 普通数组内元素, 多线程访问造成安全问题
public class AtomicArrayTest {
    public static void main(String[] args) {
        demo(
                () -> new int[10],
                array -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void
    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();
        int length = lengthFun.apply(array);

        for (int i = 0; i < length; i++) {
            // 创建10个线程, 每个线程对数组作 10000 次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }

        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束

        printConsumer.accept(array);
    }·
}
结果如下:
    [9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698]

  • 使用AtomicIntegerArray来创建安全数组
demo(
        ()-> new AtomicIntegerArray(10),
        (array) -> array.length(),
        (array, index) -> array.getAndIncrement(index),
        array -> System.out.println(array)
);

demo(
        ()-> new AtomicIntegerArray(10),
        AtomicIntegerArray::length,
        AtomicIntegerArray::getAndIncrement,
        System.out::println
);
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

保证多线程访问同一个对象的成员变量时,成员变量的线程安全性。

  • AtomicReferenceFieldUpdater—引用类型的属性
  • AtomicIntegerFieldUpdater——整形的属性
  • AtomicLongFiledUpdater——长整型的属性

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

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
  • 例子
package com.atomic;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/14 18:17
 */
@Slf4j
public class AtomicFieldTest {
    public static void main(String[] args) {
        Student stu =new Student();
        //获取原子更新器
        //泛型
        //参数1 持有属性的类 参数2 被更新的属性的类
        //newUpdater中的参数:第三个为属性的名称
        AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Student.class , String.class, "name");
//       期望的为null,如果name属性没有被别的线程更改过,默认为null,此时匹配,就可以设置name为张三
        System.out.println(updater.compareAndSet(stu,null,"张三"));
        System.out.println(updater.compareAndSet(stu,stu.name,"王五"));
        System.out.println(stu);

    }
}

class  Student{
    volatile  String name;

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

true
true
Student{name='王五'}

原子累加器(LongAddr)(重点)

  • LongAddr
  • LongAccmulator
  • DoubleAddr
  • DoubleAccumulator

累加器性能比较AtomicLong,Longaddr

package com.atomic;

import lombok.extern.slf4j.Slf4j;

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

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/15 14:55
 */
@Slf4j
public class TestAtomicLongAndLongAddr {
    public static void main(String[] args) {
        System.out.println("——-AtomicLong———");
        for (int i = 0; i < 5; i++) {
            demo(() ->new AtomicLong(),addr-> addr.getAndIncrement());
        }
        System.out.println("---LongAddr---");
        for (int i = 0; i < 5; i++) {
            demo(()->new LongAdder(),addr->addr.increment());
        }
    }


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

运行结果如下:
    ——-AtomicLong———
20000000cost394
20000000cost408
20000000cost397
20000000cost375
20000000cost413
---LongAddr---
20000000cost54
20000000cost46
20000000cost47
20000000cost44
20000000cost50

Process finished with exit code 0

LongAddr

  • **性能提升的原因很简单,就是在有竞争时,设置多个累加单元(但不会超过CPU的核心数),Thread-0累加Cell[0].而Thread-1累加Cell[1]…最终结果汇总,这样他们在累加时操作的不同的Cell变量,因此减少了CAS重试失败,从而提高性能。 **

AtomicLong

  • 之前AtomicLong等都是在一个共享资源变量上进行竞争,while(true)循环进行CAS重试,性能没有LongAdder
LongAddr原理(重点)
原理之伪共享

img

  • 缓存行伪共享得从缓存说起
  • 缓存与内存的速度比较
  • img

img

  • 因为CPU与内存的速度差异很大,需要靠预测数据缓存来提高效率,而缓存以 缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8个long)缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
  • CPU要保证数据的 一致性(缓存一致性),如果某个CPU核心 更改了数据,其他CPU核心对应的整个缓存行必须 失效

img

  • 因为Cell是数组形式,在内存中连续存储的,一个Cell为24字节(16字节的对象头和8字节的value),因此缓存航可以存下2个Cell对象。这样问题就来了:
    • Core-0要修改Cell【0】
    • Core-1要修改Cell【1】

无论谁修改成果,都会导致对方Core的缓存行失效。

  • 比如Core-0中Cell【0】=6000,cell【1】=8000要累加cell【0】=6001,cell【1】=8000,这时会让Core-1的缓存行失效
  • @sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的 前后各增加128字节大小的padding(空白),从而让CPU将对象预读至缓存时 占用不同的缓存行,这样,就不会造成对方缓存的失效。

img

源码:

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

累加流程图

img

Unsafe(重点

  • Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
  • 可以发现AtomicInteger以及其他的原子类,底层都是使用Unsafe

在这里插入图片描述

  • 使用底层的Unsafe实现原子操作
package com.atomic;

import sun.misc.Unsafe;

import java.lang.reflect.Constructor;

/**
 * @author HillCheung
 * @version 1.0
 * @date 2021/4/15 15:26
 */
public class TestUnsafe {
    public static void main(String[] args) throws  Exception{
        //通过反射获得对象
     Class unsafeClass=   Unsafe.class;
     //获取构造函数,Unsafe的构造函数为私有的.
        Constructor constructor = unsafeClass.getDeclaredConstructor();
        //设置为允许访问私有内容
        constructor.setAccessible(true);
        //创建Unsafe对象
        Unsafe uns = (Unsafe) constructor.newInstance();
        //创建Person对象
        Person person =new Person();
        //获取其属性name的偏移量
        long nameOffset = uns.objectFieldOffset(person.getClass().getDeclaredField("name"));
        long age = uns.objectFieldOffset(person.getClass().getDeclaredField("age"));
        //通过unsafe的CAS操作来改变值
        uns.compareAndSwapObject(person,nameOffset,null,"HilLCheung");
        uns.compareAndSwapInt(person,age,0,22);
        System.out.println(person);
    }
}

class Person{
    //配合CAS操作,必须使用volatile修饰
    volatile  String name;
    volatile  int age;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值