4_共享模型之无锁

共享模型之无锁

本章内容

  • CAS 与 volatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe

1. 问题提出

有如下需求,保证 account.withdraw 取款方法的线程安全

package org.example.a33;

public class AccountTest{
    public static void main(String[] args) {
        Account unsafe = new AccountUnsafe(1000);
        Account.demo(unsafe);
    }
}


class AccountUnsafe implements Account{

    private Integer balance;

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

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

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

package org.example.a33;

import java.util.LinkedList;
import java.util.List;

interface Account {
    // 获取余额的方法
    Integer getBalance();

    // 取款
    void withDraw(Integer amount);

    /**
     * 方法内启动100个线程,每个线程做-10元操作,
     * 如果初始余额为1000,那么正确的结果应该为0
     *
     */
    static void demo(Account account){
        List<Thread> ts = new LinkedList<>();
        for (int i = 0; i < 100; i++) {
            ts.add(new Thread(()->{
                account.withDraw(10);
            }));
        }

        long start = System.currentTimeMillis();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try{
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("剩余"+account.getBalance()+"钱");
        System.out.println("耗时:"+(end - start)/1000+"s");
    }
}

上面的代码在多线程环境下会有线程安全的问题,我们需要在接口的实现类中加锁:

class AccountUnsafe implements Account{

    private Integer balance;

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

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

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

这样在多线程环境下就不会出现安全了

2. 保护共享锁-无锁实现

对于上面的例子。可以使用 AQS来解决:

class  AccountCAS implements Account{

    private AtomicInteger balance;

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

    @Override
    public Integer getBalance() {
        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;
            }
        }
    }
}

3. CAS 与 volatile

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

public void withDraw(Integer amount) {
    while (true){
        // 获取余额的最新值
        int prev = balance.get();
        // 减去取款金额
        int next = prev = amount;
        // 真正修改
        if (balance.compareAndSet(prev, next)) {
            break;
        }
    }
}

其中的关键是 compareAndSet, 它的简称是 CAS(Compare And Swap),它是原子操作。CAS操作属于CPU硬件指令级别的操作。

image-20240719165812658

CAS 操作的核心就是将修改的数据写回是需要进行判断,如果当前线程所拥有的共享变量值和目前内存中的值不一致的话则修改失败!否则修改为新的值。

注意

其实 CAS 操作的底层是 lock cmpchg 指令(X86架构),在单核CPU下和多核CPU下都能够保证【比较-交换】的原子性。

在多核情况下,某个线程执行到带有 lock 的指令时,CPU会让总线锁住,当该核心把此指令执行完毕,才会再次开启总线。这个过程不会被线程的调度机制所打断,这进而保证了多线程下对内存操作的准确性,故CAS操作是原子性的。

4. volatile 关键字

使用 volatile 关键字修饰的的变量只能保证变量的可见性和有序性。即一个线程对volatile变量的修改,对另一个线程可见。但是不能保证原子性,也就是不能解决指令交错执行的问题。CAS必须借助volatile关键字才能读取到共享变量的最新值来实现【比较并交换】的功能。

5. 为什么无锁效率更高

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

因此使用CAS必须是在多核CPU下才能发挥优势,而且线程数最好不要超过CPU的核心数。因此CAS适用于线程数比较少的情况,线程数一旦比较多的话,CAS提升的效率也就没有那么明显了!

6. CAS的特点

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

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

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

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

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

7. 原子整数

JUC并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例

package org.example.a34;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);
        // i.compareAndSet(0,1);
        //System.out.println(i);
        System.out.println(i.getAndIncrement());    // 等同于 i.compareAndSet(0,1); ++i
        System.out.println(i.incrementAndGet());    // i++;
        System.out.println(i.get());
        i.decrementAndGet();    // --i
        i.getAndDecrement();    // i--

        i.getAndAdd(10);    // get i, i=i+10
        i.addAndGet(20);    //i=i+10, get i

        System.out.println("=================");
                      // 读取值   // 新值
        i.updateAndGet(o -> o * 10);    //i = i * 10;   函数式接口可以直接使用lambda表达式

        i.getAndUpdate(o-> i.get());
    }
}

8. 原子引用

  • AtomaticReference
  • AtomaticMarkableReferece
  • AtomaticStampedReferece

这里使用AtomaticReference实现一个线程安全的取钱操作:

public class A34Test {
    public static void main(String[] args) {
        DecimalAccount.demo((DecimalAccount) new BigDecimal("10000"));
    }
}


class DecimalAccountCas implements DecimalAccount{
    private AtomicReference<BigDecimal> balance;
    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

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

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


interface DecimalAccount{
    //获取余额
    BigDecimal getBalance();

    // 取款
    void withDraw(BigDecimal amount);

    static void demo(DecimalAccount account){
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(new Thread(()->{
                account.withDraw(BigDecimal.TEN);
            }));
        }

        list.forEach(Thread::start);
        list.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println(account.getBalance());
    }
}

9. ABA问题

首先有一个案例,使用原子引用修改字符串的值:

public class A37Application {

    private static final Logger log = LoggerFactory.getLogger(A37Application.class);

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

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取原子引用的值
        String pre = ref.get();
        Thread.sleep(1000);
        // 尝试将 "A"改为"C"
        log.debug("change A -> C {}",ref.compareAndSet("A", "C"));
    }
}

测试结果如下:

21:02:52.763 [main] DEBUG org.example.a37.A37Application - main start...
21:02:53.775 [main] DEBUG org.example.a37.A37Application - change A -> C true

我们发现,1s之后修改成功了!从前面我们了解到,原子引用修改成功判断的依据是线程获取的值和共享变量的最新值是不是一致,如果一致,则修改就可以成功;如果不一致,则修改不会成功。但是能不能判断这个共享变量被其它线程修改过呢?有的人认为可以,原因是将获取到的值和共享变量的值做对比,如果一样,则是没有被修改过,如果不一样,则是修改过,这样的额思路对吗?看接下来的这个例子:

package org.example.a37;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicReference;

public class A37Application {

    private static final Logger log = LoggerFactory.getLogger(A37Application.class);

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

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取原子引用的值
        String pre = ref.get();
        other();
        Thread.sleep(1000);
        // 尝试将 "A"改为"C"
        log.debug("change A -> C {}",ref.compareAndSet("A", "C"));
    }

    public 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();
    }
}

运行结果如下:

21:11:04.567 [main] DEBUG org.example.a37.A37Application - main start...
21:11:04.643 [t2] DEBUG org.example.a37.A37Application - change B -> A true
21:11:04.642 [t1] DEBUG org.example.a37.A37Application - change A -> B true
21:11:05.649 [main] DEBUG org.example.a37.A37Application - change A -> C true

我们发现,在主线程休眠期间,两个字线程对共享变量进行了修改,只不过是两个子线程恰好将字符串的最后一次值改为了"A",主线程唤醒后才继续得以执行下去。因此,主线程无法感知到其它线程是否对共享变量做出了修改!主线程只是根据最终的共享变量的值和自己最初获取到的值进行了比较,貌似看起来共享变量没变,其实已经发生了改变,这就是所谓的ABA问题:共享变量的值从A变为了B,又从B变为了A,主线程误以为共享变量没有发生变化。那么,我们如何避免ABA问题呢?虽然ABA问题并不会带来什么隐患问题:) 我们可以使用新的类来替换AtomicReference这个类让主线程能感知到其它线程对该共享变量的修改。

10. AtomicStampedReference

为了解决ABA问题,主线程希望只要有其它线程【改动过】共享变量,那么自己CAS就算失败,这时,仅仅比较是不够的,需要再加一个版本号, 谁对该共享变量做了修改,谁就要让版本号+1,这样我们就可以根据版本号可以判断该共享变量有没有被其它线程所修改过。因此,我们要将AtomicReference类替换为AtiomicStampedReference类

解决方案,如下面的代码:

package org.example.a37;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceTest {
    private static final Logger log = LoggerFactory.getLogger(AtomicStampedReferenceTest.class);
    private static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        String prev = ref.getReference();
        int stamp = ref.getStamp();
        log.debug("版本号:{}",stamp);
        other();
        Thread.sleep(1000);
        // 主线程尝试将值改为C
        log.debug("版本号:{}",stamp);
        /**
         *  V expectedReference 期望要修改的值
         *  V newReference      期望修改后的值
         *  int expectedStamp   期望的要修改版本号
         *  int newStamp        期望修改过后版本号
         */
        log.debug("change A -> C {}", ref.compareAndSet(prev,"C",stamp,stamp+1));
    }

    private static void other() throws InterruptedException {
        new Thread(()->{
            int stamp = ref.getStamp();
            log.debug("版本号:{}",stamp);
            log.debug("change A -> B {}", ref.compareAndSet(ref.getReference(), "B",stamp, stamp+1 ));
        },"t1").start();
        Thread.sleep(500);

        new Thread(()->{
            int stamp = ref.getStamp();
            log.debug("版本号:{}",stamp);
            log.debug("change B -> A {}", ref.compareAndSet(ref.getReference(), "B",stamp, stamp+1 ));
        },"t2").start();
    }
}

运行如下:

20:51:36.031 [main] DEBUG o.e.a37.AtomicStampedReferenceTest - main start...
20:51:36.037 [main] DEBUG o.e.a37.AtomicStampedReferenceTest - 版本号:0
20:51:36.120 [t1] DEBUG o.e.a37.AtomicStampedReferenceTest - 版本号:0
20:51:36.120 [t1] DEBUG o.e.a37.AtomicStampedReferenceTest - change A -> B true
20:51:36.625 [t2] DEBUG o.e.a37.AtomicStampedReferenceTest - 版本号:1
20:51:36.625 [t2] DEBUG o.e.a37.AtomicStampedReferenceTest - change B -> A true
20:51:37.636 [main] DEBUG o.e.a37.AtomicStampedReferenceTest - 版本号:0
20:51:37.636 [main] DEBUG o.e.a37.AtomicStampedReferenceTest - change A -> C false

我们发现,修改成功! 我们发现,使用AtomicStampedReference类解决了ABA问题。当主线程想要修改时发现此时的版本号发生了修改,也就是说该共享变量被别的线程修改了,因此此次CAS修改操作失败。

同时呢,我们使用AtomicStampedReference还可以追踪共享变量中途被修改了几次,其依据就是版本号的变化。

11. AtomicMarkableReference

从上面可以知道,我们可以使用AtomicStampedReference解决了ABA问题,并且还可以追踪共享变量中途被修改了几次,其依据就是版本号的变化。但是有些时候我们并不关系共享变量在途中被修改了几次,我们只是单纯的关心共享变量是否被修改过,于是就有了AtomicMarkableReference类,AtomicMarkableReference类相当于AtomicStampedReference类的简化版,并不是用整数的版本号来记录修改状态,而是使用布尔值来记录该共享变量是否被修改过。因此无法判断出共享变量发生了几次变化,而只能判断共享变量是否被修改过。

倒空

检查

已满

为空

保洁阿姨

主人

垃圾袋

新垃圾袋

示例代码如下:

package org.example.a37;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicMarkabledReferenceTest {
    private static final Logger log = LoggerFactory.getLogger(AtomicMarkabledReferenceTest.class);

    public static void main(String[] args) throws InterruptedException {
        Garbage bag = new Garbage("装满了垃圾");
        // 参数2为true可以看做一个标记,表示垃圾袋中的垃圾满了
        AtomicMarkableReference<Garbage> ref = new AtomicMarkableReference<>(bag,true);
        log.debug("main start...");
        Garbage pre = ref.getReference();
        log.debug(pre.toString());

        new Thread(()->{
            log.debug("t1 start...");
            bag.setDesc("空垃圾袋");
            ref.compareAndSet(bag,bag,true,false);
        },"保洁阿姨").start();

        Thread.sleep(1000);
        // 判断是否要换新的垃圾袋
        boolean success = ref.compareAndSet(pre, new Garbage("空垃圾袋"), true, false);
        log.debug("换了吗? {}",success);
        log.debug(ref.toString());
    }
}

class Garbage{
    String desc;

    public Garbage(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

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

    @Override
    public String toString() {
        return "Garbage{" +
                "desc='" + desc + '\'' +
                '}';
    }
}
21:25:28.060 [main] DEBUG o.e.a37.AtomicMarkabledReferenceTest - main start...
21:25:28.066 [main] DEBUG o.e.a37.AtomicMarkabledReferenceTest - Garbage{desc='装满了垃圾'}
21:25:28.148 [保洁阿姨] DEBUG o.e.a37.AtomicMarkabledReferenceTest - t1 start...
21:25:29.154 [main] DEBUG o.e.a37.AtomicMarkabledReferenceTest - 换了吗? false
21:25:29.156 [main] DEBUG o.e.a37.AtomicMarkabledReferenceTest - java.util.concurrent.atomic.AtomicMarkableReference@6591f517

我们发现保洁阿姨对垃圾袋对象进行修改后,主线程CAS操作失败了,因为当前垃圾袋对象已经被修改过了!

12. 原子数组

之前介绍了AtomicReference,它解决了多个线程对共享变量访问时的线程安全性,但是呢,我们有的情况并不是修改引用本身,而是修改引用里面的内容,例如数组!我只想修改数组中的内容,而不是修改数组的引用。这个时候就需要使用到原子数组了。原子数组包括如下三个类别:

  1. AtomicIntegerArray:保护的数组中数据类型是int的
  2. AtomicLongArray:保护数组中数据类型是Long类型的
  3. AtomicReferenceArray:保护数组中数据类型是引用类型的

下面介绍一下原子数组的常见方法。

  • AtomicIntegerArray(int length):创建一个具有指定长度的 AtomicIntegerArray,并且所有元素初始化为零。
  • AtomicIntegerArray(int[] array):根据给定的 int 数组创建一个 AtomicIntegerArray
  • int get(int index):返回指定索引处的元素值。
  • void set(int index, int newValue):将指定索引处的元素设置为新的值
  • int getAndSet(int index, int newValue):原子性地设置指定位置的值,并返回旧值
  • boolean compareAndSet(int index, int expect, int update):如果当前值等于期望值,则以原子方式将该位置的值设置为更新值。
  • int getAndIncrement(int index):原子性地将指定位置的值加一,并返回旧值。
  • int getAndDecrement(int index):原子性地将指定位置的值减一,并返回旧值。
  • int getAndAdd(int index, int delta):原子性地将指定位置的值加上给定值,并返回旧值。
  • int incrementAndGet(int index):原子性地将指定位置的值加一,并返回新值。
  • int decrementAndGet(int index):原子性地将指定位置的值减一,并返回新值。
  • int addAndGet(int index, int delta):原子性地将指定位置的值加上给定值,并返回新值。
package org.example.a38;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Test38 {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        array.set(0, 10);
        System.out.println("Initial value at index 0: " + array.get(0));

        array.getAndIncrement(0);
        System.out.println("Value after getAndIncrement: " + array.get(0));

        array.getAndAdd(0, 5);
        System.out.println("Value after getAndAdd with 5: " + array.get(0));
    }
}

剩余两个的用法和AtomicIntegerArray的用法类似,这里就不再赘述。

13. 字段更新器

  • AtomicReferenceFieldUpdater //域 字段为引用类型
  • AtomicIntegerFieldUpdater // 字段为int
  • AtomicLongFieldUpdater // 字段为Long

字段更新器保护的是对象里面的某个属性、成员变量的某个域进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
	at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.<init>(AtomicReferenceFieldUpdater.java:348)
	at java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdater.java:110)
	at org.example.a39.Test39.main(Test39.java:15)
package org.example.a39;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class Test39 {

    private static Logger log = LoggerFactory.getLogger(Test39.class);

    public static void main(String[] args) {
        Student student = new Student();
        // 模拟多线程对该对象的属性进行修改
        AtomicReferenceFieldUpdater updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        if (updater.compareAndSet(student, null, "小红")) {
            log.debug("update student{}",student);
        }

    }
}

class Student {
    volatile String name;

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

结果如下:

11:41:20.741 [main] DEBUG org.example.a39.Test39 - update studentStudent{name='小红'}

14. 原子累加器

累加器故名思意就是做累加操作的,有的人就问了,AtomicLong, AtommicInteger也可以做原子累加啊,为什么还会出现一个累加器呢?这是因为,在JDK8以后新增了几个专门的用于做累加操作的类LongAdder, DoubleAdder,LongAccumulator,DoubleAccumulator,它们的性能要比AtomicInteger,AtomicLong做累加操作要高很多。

我们首先对两者的性能做一个比较:

package org.example.a40;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class Test40 {
    private static final Logger log = LoggerFactory.getLogger(Test40.class);
    public static void main(String[] args) throws InterruptedException {
        AtomicLongTest();
        Thread.sleep(5000);
        LongAddrTest();
    }

    private static void AtomicLongTest(){
        List<Thread> threads = new ArrayList<>(4);
        AtomicLong atomicLong = new AtomicLong();
        long start = System.nanoTime();
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(()->{
                for (int j = 0; j < 500000; j++) {
                    atomicLong.getAndIncrement();
                }
            }));
        }
        threads.forEach(t->t.start());

        long end = System.nanoTime();
        log.debug("AtomicInteger cost time:{}",end - start);
    }

    private static void LongAddrTest(){
        List<Thread> threads = new ArrayList<>();
        LongAdder adder = new LongAdder();
        long start = System.nanoTime();
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(()->{
                for (int j=0;j<500000;j++){
                    adder.increment();
                }
            }));
        }
        threads.forEach(t->t.start());
        long end = System.nanoTime();
        log.debug("LongAddr const time:{}",end - start);
    }
}

测试结果如下:

12:07:45.246 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:66253400
12:07:50.266 [main] DEBUG org.example.a40.Test40 - LongAddr const time:2992200

我们发现,使用LongAddr做加法操作比AtomicLong要快上非常多,我们载多运行几次看看:

package org.example.a40;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class Test40 {
    private static final Logger log = LoggerFactory.getLogger(Test40.class);
    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<5;i++){
            AtomicLongTest();
        }
        Thread.sleep(5000);
        for (int i=0;i<5;i++){
            LongAddrTest();
        }
    }

    private static void AtomicLongTest(){
        List<Thread> threads = new ArrayList<>(4);
        AtomicLong atomicLong = new AtomicLong();
        long start = System.nanoTime();
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(()->{
                for (int j = 0; j < 500000; j++) {
                    atomicLong.getAndIncrement();
                }
            }));
        }
        threads.forEach(t->t.start());

        long end = System.nanoTime();
        log.debug("AtomicInteger cost time:{}",end - start);
    }

    private static void LongAddrTest(){
        List<Thread> threads = new ArrayList<>();
        LongAdder adder = new LongAdder();
        long start = System.nanoTime();
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(()->{
                for (int j=0;j<500000;j++){
                    adder.increment();
                }
            }));
        }
        threads.forEach(t->t.start());
        long end = System.nanoTime();
        log.debug("LongAddr const time:{}",end - start);
    }
}
12:58:09.202 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:65604500
12:58:09.208 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:426200
12:58:09.208 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:319500
12:58:09.209 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:392000
12:58:09.209 [main] DEBUG org.example.a40.Test40 - AtomicInteger cost time:333800
12:58:14.218 [main] DEBUG org.example.a40.Test40 - LongAddr const time:795000
12:58:14.218 [main] DEBUG org.example.a40.Test40 - LongAddr const time:347600
12:58:14.220 [main] DEBUG org.example.a40.Test40 - LongAddr const time:1518100
12:58:14.221 [main] DEBUG org.example.a40.Test40 - LongAddr const time:265400
12:58:14.221 [main] DEBUG org.example.a40.Test40 - LongAddr const time:198300

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

15. LongAdder 源码

15.1 CAS锁实现锁的原理

如下面的例子:

package org.example.a41;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicInteger;

public class Test41 {

}

class LockCas {
    private AtomicInteger state = new AtomicInteger(0); //0表示没加锁,1表示加了锁
    private static final Logger log = LoggerFactory.getLogger(LockCas.class);

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

    public void unlock() {
        log.debug("unlock");
        state.compareAndSet(1, 0);
    }

    public static void main(String[] args) {
        LockCas lockCas = new LockCas();
        new Thread(() -> {
            log.debug("t1 begin...");
            lockCas.lock();
            try {
                log.debug("t1 lock...");
                Thread.sleep(1000);
            } catch(InterruptedException e){
                    throw new RuntimeException(e);
                } finally {
                lockCas.unlock();
            }
        }, "t1").start();

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

运行结果如下:

14:02:05.366 [t1] DEBUG org.example.a41.LockCas - t1 begin...
14:02:05.366 [t2] DEBUG org.example.a41.LockCas - t2 begin...
14:02:05.370 [t1] DEBUG org.example.a41.LockCas - t1 lock...
14:02:06.376 [t1] DEBUG org.example.a41.LockCas - unlock
14:02:06.376 [t2] DEBUG org.example.a41.LockCas - t2 lock...
14:02:06.376 [t2] DEBUG org.example.a41.LockCas - unlock

15.2 累加单元Cell原理之伪共享

其中的Cell就是累加单元。查看Cell类的源码:

@sun.misc.Contended // 防止缓存行的伪共享
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 最重要的方法,用来使用CAS方式进行累加操作,prev代表的是旧值,next代表的是新值
    final boolean cas(long prev, long next) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
    // 省略不重要的代码
}

我们发现Cell类上有一个非常重要的方法,就是cas方法,其实就是使用CAS来更改值。此外,还有一个注解:@sun.misc.Contended,这个注解的作用就是防止缓存行的伪共享

这个得先从CPU缓存说起。下面是一个CPU与内存之间的内存结构图:

image-20240725141251812

从CPU到 大约需要的时钟周期
register 1 cycle(4GHz的CPU约为0.25ns)
L1 3-4 cycle
L2 10-20 cycle
L3 40-45 cycle
main memory 120-240 cycle

因此CPU与内存之间的速度差异很大,需要靠预读数据至缓存来提升效果

而缓存,是以缓存行为单元,每个缓存行对应着一块内存,一般是64byte(8个long)

缓存的加入会造成数据副本的产生(同一份数据会缓存在不同核心的缓存行中中),举一个例子:

主存中有一个数据A,现在两个核心的CPU都需要使用这个数据A,于是,CPU-0会将数据读到自己的缓存中,CPU-1也会将数据读到自己的缓存中。问题来了,如果CPU-0对数据A进行了修改,CPU-1也需要将自己缓存中的数据进行修改,改成和CPU-0中一致的数据,要不然,数据就会不一致,因此,一个核心对缓存中的数据进行了修改,就会让另一个核心在它自己的缓存中让这个数据失效,让该核心从缓存中读取最新的数据,这就是多级缓存一致性问题 MESI

CPU要保证多级缓存中数据的一致性问题,如果某个核心更改了数据,其它CPU核心对应的整个缓存行必须失效

image-20240725150000328

因为累加单元Cell是数组的形式,在内存中是连续存储的,一个累加单元Cell大小为24byte(18byte对象头+8byte的long value)。因此一个缓存行可以存的下2个Cell对象。如上面的图。但是问题来了:

  • Core-0要修改Cell[0]
  • Core-1要修改Cell[1]

无论谁修改成功,都要将修改后的数据回写到内存中,并且将对方存有Cell[0]和Cell[1]的缓存行失效,比如Core-0中Cell[0]=6000, Cell[1]=7000要累加到Cell[0]=6001,Cell[1]=7001,这时Core-1中的对应的缓存行失效了,需要重新从主存中读取最新值。这样每次线程执行完操作都会导致对方的缓存行中存储的累加单元都失效,这样效率也太低了!有没有一种办法可以避免这种情况呢?答案是有的,如下面的图:

image-20240725150954320

我们可以对每个累加单元进行扩充,使之每个缓存行职能存储一个累加单元。这样两个累加单元可以存储到不同的CPU缓存行中。如上面的图,不同的Cell被加载到不同的CPU缓存中。这样在多线程工作的时候就会在逻辑上产生独立:各自做各自的工作,互不干扰。让CPU预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行数据失效。最后计算完下只需要将各自的结果写回主存中相应的位置即可。这也就是@Contended注解将缓存行伪共享的原理。

15.3 LongAdder中add方法原理

image-20240725152235574

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);
    }
}

15.4 LongAccumulate 分析

longAccumulate方法非常的重要,可以说是涵盖了大部分逻辑,里面的代码也是非常的复杂!

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // cells数组不为空
            if ((as = cells) != null && (n = as.length) > 0) {
                // 判断当前线程有没有累加单元
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        // 创建一个Cell对象,同时将初始累加值传递进入
                        Cell r = new Cell(x);   // Optimistically create
                        // 当前cells锁如果没有被其它的线程占用,自己使用CAS尝试占用锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                // 再次确保数组不为空
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    // 将创建好的Cell对象存入到数组的空闲位置处
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                // 释放锁,并直接退出返回
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :	// 使用CAS进行累加
                                             fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)	// 数组长度是否大于CPU个数
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                // 没有超过CPU上限值,尝试加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];	Cell数组扩容2for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);			// 改变线程对应的Cell对象,再做下一步的尝试
            }
            // 当前cell数组没有被加锁并且没有其它线程去改变这个数组鼻并且给cas数组上锁(使用CAS方法上锁)
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table,再次检查有没有其它线程把cells数组创建好了
                    if (cells == as) {
                        Cell[] rs = new Cell[2]; 	// cell数组的初始化大小为2
                        rs[h & 1] = new Cell(x);	//  给一个初始值1(+1操作)
                        cells = rs;					// 将数组复制给累加单元数组(Cell数组时懒惰初始化的,只有用的时候才会真正的初始化)
                        init = true;				// 表明已经初始化好了
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;							// 初始化好直接退出循环
            }
            // 如果对cells数组加锁失败,使用cas对base进行累加
            else if (casBase(v = base, ((fn == null) ? v + x	// 如果累加成功,直接return,
                                        :fn.applyAsLong(v, x)))) // 否则重新进入循环入口,进行下一轮尝试
                break;                          // Fall back on using base
        }
    }

15.5 sum方法的分析

获取最终的结果是通过sum()方法获取的,我们查看一下sum()方法的源码:

public long sum() {
    // 拿到累加单元数组
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        // 将每个累加单元中的数组都拿出来做累加操作(注意累加操作从base处开始累加)
        // 这样就可以拿到最终的结果了
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

16. Unsafe类

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

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class TestUnsafe {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        int a = 10;
        // 拿到对象Unsafe
        Field theUnsafe= Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        System.out.println(unsafe);
    }
}

结果如下:

sun.misc.Unsafe@2503dbd3

16.1 Unsafe CAS操作

如下面的示例代码:

package org.example.a42;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class A42Test {
    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(Teacher.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
        // 2. 执行CAS操作
        Teacher t = new Teacher();
        unsafe.compareAndSwapInt(t, idOffset,0,1);
        unsafe.compareAndSwapObject(t,nameOffset,null,"张三");
        // 3. 验证结果
        System.out.println(t.toString());
    }
}

class Teacher{
    volatile int id;
    volatile String name;

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

运行结果如下:

sun.misc.Unsafe@2503dbd3
Teacher{id=1, name='张三'}

16.2 手写一个原子整数类

手写一个原子整数类来实现AtomicInteger的功能,代码如下:

class MyAtomicInteger {
    private volatile int value;  //要保护的成员变量
    private static final long valueOffset;
    static final Unsafe UNSAFE;
    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe)theUnsafe.get(null);
            // 计算value的地址偏移量
            valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

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

    public int getValue(){
        return value;
    }

    // 修改值
    public void compareAndSet(int v){
        while (true){
            int prev = this.value;
            int next = prev+v;
            if (UNSAFE.compareAndSwapInt(this, valueOffset,prev,next)) {
                break;
            }
        }
    }
}
posted @   LilyFlower  阅读(6)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示