1、问题

多线程模拟银行取钱操作,代码如下:

public interface Demo4 {
    //获取当前的余额
    Integer getBalance();
    // 取款
    void withdraw(Integer amount);
    //操作某一个账户
    static void demo(Demo4 account) {
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>(1000);

        //创建1000个线程,每个线程取款10
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.withdraw(10);
            });
            ts.add(thread);
        }
        //启动线程
        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_00 + "ms");

    }
}

新建一个实现类,无锁实现类:

public class Demo4Impl1 implements Demo4 {

    private Integer balance;

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

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

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

    public static void main(String[] args) {
        //存在线程安全问题
        Demo4.demo(new Demo4Impl1(10000));
    }
}

打印结果肯定是不正确的

7740 cost : 1715ms

解决方案一:

增加synchronized关键,加锁解决。

public class Demo4Impl2 implements Demo4 {

    private Integer balance;

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

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

    public static void main(String[] args) {
        //加锁解决
        Demo4.demo(new Demo4Impl2(10000));
    }
}

解决方案二:

使用Java ->JUC内置的数据类型,AtomicInteger类型

public class Demo4Impl3 implements Demo4 {
    //原子int类型
    private AtomicInteger balance;

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

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

    @Override
    public void withdraw(Integer amount) {
//        while (true){
//            int prev = balance.get();
//            int next = prev - amount;
//            if(balance.compareAndSet(prev,next)){
//                break;
//            }
//        }
        //简化写法
        balance.addAndGet(-1 * amount);
    }

    public static void main(String[] args) {
        //无锁解决
        Demo4.demo(new Demo4Impl3(10000));
    }
}
0 cost : 2683ms

2、CAS和volatile

AtomicInteger内部没有锁是怎么实现线程安全的?核心代码分析

@Override
public void withdraw(Integer amount) {
    while (true){
        //当前值
        int prev = balance.get();
        //下次需要更新的值
        int next = prev - amount;
        //cas操作
        //赋值前先拿balance和当前值prev进行比较
        //如果两个值相同,才会更新next
        //如果不相等则会进行下一次的  while循环
        if(balance.compareAndSet(prev,next)){
            break;
        }
    }
}
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交 换】的原子性。

但是AtomicInteger内维护的value值必须是volatile修饰的变量,看源码可以看到,构造方法赋值给value

//保证多线程访问下,数据的可见性
rivate volatile int value;

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

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

再次强调

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

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

3、无锁效率是否更高?

  • 无锁下,即使重试失败线程仍然在进行while循环,没有停止。synchronized会让线程在没有获得到锁的时候,发生上下文切换。进入阻塞状态。重新回复线程的运行状态耗时较长。
  • 无锁状态,想要保证线程的告诉运转,需要cpu的支持,如果cpu核数较少,也是会造成无法获得执行机会,仍然会导致上下文切换的发生。

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

  • cas是乐观锁的思想,其他线程修改后继续重试
  • synchronized是悲观锁思想,只有拿到锁才会进行操作,提防其他线程修改变量
  • cas是体现无锁并发,无阻塞并发。因为没有使用synchronized,线程不会阻塞,这是效率提升的因素之一。

    但是如果线程之间竞争激烈,会造成重试几率增大频繁发生,反而影响性能。

文章目录