并发编程——共享模型之无锁

本文深入探讨并发编程中的无锁概念,通过分析CAS(compareAndSet)机制,解决ABA问题,以及介绍AtomicInteger等原子类的使用。重点讲解了LongAdder的高效性能和其背后的原理,包括Cell数组、CAS锁和伪共享问题。同时,讨论了Unsafe类在并发编程中的作用及其底层操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、并发编程无锁案例

无锁高效率

二、CAS(compareAndSet)

 CAS和volatile的关系

CAS的特点

三、ABA问题 

解决

 四、CAS操作的原子类

 原子整数

 原子引用

 原子数组

字段更新器

原子累加器

五、原子累加器

原子累加器与原子整数的性能比较

六、原子累加器LongAdder原理

LongAdder 类有几个关键域

CAS锁 

伪共享

LongAdder源码

七、Unsafe

 Unsafe的CAS操作实现


一、并发编程无锁案例

interface Account {
// 获取余额
 Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 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::start);

//等待每一个线程执行完
ts.forEach(t -> {
try {
t.join();
 } catch (InterruptedException e) {
e.printStackTrace();
 }
 });

    long end = System.nanoTime();

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

//实现类
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) {
balance -= amount;
 }
}

上述案例很明显,withdraw方法是不安全的,以前通常的选择是在withdraw上加synchronized实现对多线程操作共享变量的阻塞,但是现在要引入CAS乐观锁的思路

class AccountSafe implements Account {
    //引用原子整数类
    private AtomicInteger balance;
public AccountSafe(Integer balance) {
    this.balance = new AtomicInteger(balance);
}
    @Override
    public Integer getBalance() {
     return balance.get();
 }
    @Override
 public void withdraw(Integer amount) {
   // 需要不断尝试,直到成功为止
   while (true) {
    //获得此刻balance的最新值 1000
    Int prev = balance.get();
    //在最新值的基础上做减法 990
    int next = prev - amount;
    //compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回false 表示失败
    //比如:
    //拿到的最新值1000,别的线程已经做了减法变成了990,那么1000与990做比较发现不一致
    //那么本线程的这次next=990就作废了
    //进入while 下次循环重试一致,以 next 设置为新值,返回 true 表示成功,这就是CAS操作的原理
    if (balance.compareAndSet(prev, next)) {
       break;
  }
 }
   // 可以简化为下面的方法
  // balance.addAndGet(-1 * amount);
  }
}

  CAS操作实际上就是拿着线程缓存空间的值和当前的主存中的值做比较,相同证明没有其他线程改变它,则执行对其修改的操作,否则更新缓存空间的值,再与主存中的值做比较等待修改的过程

  • 无锁高效率

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。

但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

二、CAS(compareAndSet)

对上述案例的执行图分析​​​​​​​​​​​​​​

​​​​​​​​​​​​​​​​​​​​​​​​​​​​在Java内存模型中,每个线程有一个属于自己的缓存区,而多个线程共享一个主内存,在并发环境中,多个线程从主内存中读取数据缓存到自己的缓存区,当执行写操作时再写入缓存区,因此,CAS涉及的变量为:一个旧值(线程缓存区的值),一个主存值(主内存中的值),一个新值。当一个线程执行该操作时,需要先将旧值和主存值比较,只有当二者相等时,才会执行新值的更新操作,并同时将新值写入主存,即也会更新主存值。当一个线程执行CAS操作时,另一个线程会自旋等待,直到执行到CAS操作

  •  CAS和volatile的关系

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

来看看AtomicInterger内是怎么使用volatie的

private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
public AtomicInteger(int initialValue) {
        value = initialValue;
    }

可以很明显地看出,在原子类的初始化的时候,值是赋给了一个被volatile修饰的value,保证时刻获取到的都是主存中的最新值。 

  • CAS的特点

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

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

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

CAS 体现的是无锁并发、无阻塞并发,因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生(自旋)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值