java多线程之-CAS无锁

1.背景

加锁确实能解决线程并发的的问题,但是会造成线程阻塞等待等问题

那么有没有一种方法,既可以线程安全,又不会造成线程阻塞呢?

答案是肯定的……请看如下案例

注意:重要的文字说明,写在了代码注释上,这样便于大家理解,请阅读代码和注释加以理解;

2.取钱案例引出问题

启动10000个线程,每个线程减去10元

原来账户共10000 0元

正常情况账户最后的余额应该是0元

测试多线程并发问题

先定义一个通用的接口,后面使用不同实现来测试

账户Money接口:

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05;

import java.util.ArrayList;import java.util.List;

/* * @author 姿势帝-博客园 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 02/16 8:14 * @description /public interface Money { // 获取余额 Integer getBalance();
</span><span>//</span><span> &#x53D6;&#x6B3E;</span>
<span>void</span><span> reduce(Integer amount);

</span><span>/**</span><span>
 * &#x542F;&#x52A8;10000&#x4E2A;&#x7EBF;&#x7A0B;,&#x6BCF;&#x4E2A;&#x7EBF;&#x7A0B;&#x51CF;&#x53BB;10&#x5143;
 * &#x539F;&#x6765;&#x8D26;&#x6237;&#x5171;10000 0&#x5143;
 * &#x6B63;&#x5E38;&#x60C5;&#x51B5;&#x5E94;&#x8BE5;&#x662F;0&#x5143;
 * &#x6D4B;&#x8BD5;&#x591A;&#x7EBF;&#x7A0B;&#x5E76;&#x53D1;&#x95EE;&#x9898;
 *
 * </span><span>@param</span><span> account
 </span><span>*/</span>
<span>static</span> <span>void</span><span> handel(Money account) {
    List</span><thread> ts = <span>new</span> ArrayList&lt;&gt;<span>();
    </span><span>long</span> start =<span> System.nanoTime();
    </span><span>for</span> (<span>int</span> i = 0; i &lt; 10000; i++<span>) {
        ts.add(</span><span>new</span> Thread(() -&gt;<span> {
            account.reduce(</span>10<span>);
        }));
    }
    ts.forEach(Thread::start);
    ts.forEach(t </span>-&gt;<span> {
        </span><span>try</span><span> {
            t.join();
        } </span><span>catch</span><span> (InterruptedException e) {
            e.printStackTrace();
        }
    });
    </span><span>long</span> end =<span> System.nanoTime();
    System.out.println(</span>&quot;&#x5F53;&#x524D;&#x4F59;&#x989D;:&quot; +<span> account.getBalance()
            </span>+ &quot; ,&#x8017;&#x65F6;: &quot; + (end - start) / 1000_000 + &quot; ms&quot;<span>);
}

}

View Code

2.1.存在线程安全的解决方案

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05.impl;

import com.ldp.demo05.Money;

/**
 * @author 姿势帝-博客园
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 02/16 8:17
 * @description
 */
public class UnSafeMoney implements Money {
    private Integer balance;

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

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

    @Override
    public void reduce(Integer amount) {
        // 存在线程不安全
        balance -= amount;
    }
}

View Code

2.2.使用传统锁的解决方案

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05.impl;

import com.ldp.demo05.Money;

/**
 * @author 姿势帝-博客园
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 02/16 8:17
 * @description
 */
public class SyncSafeMoney implements Money {
    private Integer balance;

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

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

    @Override
    public synchronized void reduce(Integer amount) {
        // 当前对象加锁 安全
        balance -= amount;
    }
}

View Code

2.3.使用CAS无锁的解决方案

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05.impl;

import com.ldp.demo05.Money;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 姿势帝-博客园
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 02/16 8:30
 * @description
 * 无锁的思路
 *
 *
 */
public class CASSafeMoney implements Money {
    private AtomicInteger balance;

    public CASSafeMoney(Integer balance) {
        this.balance = new AtomicInteger(balance);
    }

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

    /**
     * compareAndSet 做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败
     *
     * 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
     * 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再
     * 开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
     *
     * CAS 的特点
     * 结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
     * 1.CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试在执行【修改】。
     * 2.synchronized 是基于悲观锁的思想:最悲观的估计,防着其它线程来修改共享变量,当前线程上锁后,其他线程阻塞等待。
     * 3.CAS 体现的是【无锁并发、无阻塞并发】。
     * 因为没有使用 synchronized,所以线程【不会陷入阻塞】,这是效率提升的因素之一,
     * 但如果竞争激烈,可以想到重试必然频繁发生,反而频繁切换上下文,效率会受影响。
     * 4.特别注意:
     * 无锁情况下,但如果竞争激烈,因为线程要保持运行,需要CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
     *
     * @param amount
     */
    @Override
    public void reduce(Integer amount) {
        // 不断尝试直到成功为止
        while (true) {
            // 修改前的值
            int prev = balance.get();
            // 修改后的值
            int next = prev - amount;
            // 执行修改 compareAndSet  使用CAS乐观锁实现
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
        // 简要写法
        // balance.addAndGet(-1 * amount);
    }
}

View Code

3.测试

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05;

import com.ldp.demo05.impl.CASSafeMoney;

/**
 * @author 姿势帝-博客园
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 02/16 8:20
 * @description
 */
public class Test01Money {
    public static void main(String[] args) {
        // 1.无锁不安全 当前余额:29530 ,耗时: 4847 ms
        // Money.handel(new UnSafeMoney(100000));

        // 2.synchronized加锁安全 当前余额:0 ,耗时: 7386 ms
        // Money.handel(new SyncSafeMoney(100000));

        // 3.使用乐观锁 CAS  当前余额:0 ,耗时: 3466 ms
        Money.handel(new CASSafeMoney(100000));
    }
}

View Code

4.CompareAndSet 方法分析

java多线程之-CAS无锁java多线程之-CAS无锁
package com.ldp.demo05;

import com.common.MyThreadUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 姿势帝-博客园
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 02/16 8:49
 * @description
 */
@Slf4j
public class Test02CompareAndSet {
    /**
     * 观察多线程修改值
     *
     * @param args
     */
    public static void main(String[] args) {
        AtomicInteger n = new AtomicInteger(100);
        int mainPrev = n.get();
        log.info("当前值:{}", n.get());
        // 线程 t1 将其修改为 90
        new Thread(() -> {
            // 模拟睡眠3秒
            MyThreadUtil.sleep(1);
            boolean b = n.compareAndSet(mainPrev, 90);
            log.info("修改结果:{}", b);
        }, "t1").start();

        // 模拟睡眠3秒
        MyThreadUtil.sleep(2);
        new Thread(() -> {
            boolean b = n.compareAndSet(mainPrev, 80);
            log.info("修改结果:{}", b);
        }, "t2").start();
        // 最后结果值
        MyThreadUtil.sleep(2);
        log.info("最后值为={}", n.get());
        // 21:04:15.369 [main] -> 当前值:100
        // 21:04:16.451 [t1] -> 修改结果:true
        // 21:04:17.457 [t2] -> 修改结果:false
        // 21:04:19.457 [main] -> 最后值为=90
    }
}

View Code

完美!

Original: https://www.cnblogs.com/newAndHui/p/15912116.html
Author: 李东平|一线码农
Title: java多线程之-CAS无锁

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/604460/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球