一、Sync VS CAS

文章讨论了在Java中如何处理并发控制,对比了使用`synchronized`关键字和`AtomicInteger`的`CAS`操作。通过示例展示了`synchronized`如何实现线程同步,以及`AtomicInteger`如何通过无锁机制提高性能。同时,文章还介绍了`CAS`的ABA问题及其解决方案`AtomicStampReference`。
摘要由CSDN通过智能技术生成

1、初步引入Sync和CAS

在同一package下,有如下代码:

public class A {

    int num = 0;

    public long getNum(){
        return num;
    }

    public void increase(){
        num++;
    }
}
public class LockTest {

    public static void main(String[] args) throws InterruptedException {
        A a = new A();

        long start = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000000000; i++) {
                a.increase();
            }
        });
        t1.start();

        for (long i = 0; i < 1000000000; i++) {
            a.increase();
        }
        t1.join();

        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms",end - start));
        System.out.printf("%sms",end - start);
        System.out.println();
        System.out.println(a.getNum());
    }

}

分析代码,我们知道因为没有对线程进行一定的并发控制,它的结果会不是预期的值(2000000000)。以下是两次运行的结果,因为每次并发时的情况不同结果一般不同:

 当我们修改代码时(在increase方法上加上synchronized锁时),注意这里加的锁它锁的是类的当前的对象,也就是类A的当前对象(this)

synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;

  1. Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。

synchronized修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

  1. Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

具体看下面博客:
Synchronized关键字加在普通方法上和加在静态方法上有什么区别?https://blog.csdn.net/weixin_43658899/article/details/107230699#:~:text=%E4%B8%80%E3%80%81Synchronized%E5%85%B3%E9%94%AE%E5%AD%97%E5%8A%A0%E5%9C%A8%E6%99%AE%E9%80%9A%E6%96%B9%E6%B3%95%E4%B8%8A%E5%92%8C%E5%8A%A0%E5%9C%A8%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E4%B8%8A%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%3F%20synchronized%E4%BF%AE%E9%A5%B0%E4%B8%8D%E5%8A%A0static%E7%9A%84%E6%96%B9%E6%B3%95%EF%BC%8C%E9%94%81%E6%98%AF%E5%8A%A0%E5%9C%A8%E5%8D%95%E4%B8%AA%E5%AF%B9%E8%B1%A1%E4%B8%8A%EF%BC%8C%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%B2%A1%E6%9C%89%E7%AB%9E%E4%BA%89%E5%85%B3%E7%B3%BB%EF%BC%9B,synchronized%E4%BF%AE%E9%A5%B0%E5%8A%A0%E4%BA%86static%E7%9A%84%E6%96%B9%E6%B3%95%EF%BC%8C%E9%94%81%E6%98%AF%E5%8A%A0%E8%BD%BD%E7%B1%BB%E4%B8%8A%EF%BC%8C%E8%BF%99%E4%B8%AA%E7%B1%BB%E6%89%80%E6%9C%89%E7%9A%84%E5%AF%B9%E8%B1%A1%E7%AB%9E%E4%BA%89%E4%B8%80%E6%8A%8A%E9%94%81%E3%80%82%20Synchronized%E4%BF%AE%E9%A5%B0%E9%9D%9E%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%EF%BC%8C%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E5%AF%B9%E8%B0%83%E7%94%A8%E8%AF%A5%E6%96%B9%E6%B3%95%E7%9A%84%E5%AF%B9%E8%B1%A1%E5%8A%A0%E9%94%81%EF%BC%8C%E4%BF%97%E7%A7%B0%E2%80%9C%E5%AF%B9%E8%B1%A1%E9%94%81%E2%80%9D%E3%80%82

public synchronized void increase(){
        num++;
    }

我们能够达到正确的结果:

 显而易见的是,这种方式处理并发是比较吃力的。

上图是对synchronized的一个解释图。synchronized锁对this对象进行加锁,实际上是对this对象中的monitor对象进行加锁。线程1对方法进行调度,线程2、3等待线程1锁释放(等待队列)

去了解java中的monitor机制,讲得确实挺不错的https://zhuanlan.zhihu.com/p/356010805

 (有空细究)

下面介绍该问题另外一种控制并发的方法,下面是更改后的代码:

public class A {

    AtomicInteger num = new AtomicInteger();

    public long getNum(){
        return num.get();
    }

    public void increase(){
        num.incrementAndGet();
    }
}
public class LockTest {

    public static void main(String[] args) throws InterruptedException {
        A a = new A();

        long start = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000000000; i++) {
                a.increase();
            }
        });
        t1.start();

        for (long i = 0; i < 1000000000; i++) {
            a.increase();
        }
        t1.join();

        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms",end - start));
        System.out.printf("%sms",end - start);
        System.out.println();
        System.out.println(a.getNum());
    }

}

 运行结果如下,很明显要快很多,从102402ms到15886ms

2、CAS

1、AtomicInteger底层实现原理是什么?

AutomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,原子操作的实现是基于cas(compare-and-swap)来实现的。

2、什么是原子性访问?

一组操作要么全部成功,要么全部失败。

以下是对AutomicInteger这一并发类核心代码的实现:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
    // 全局唯一id
    static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 每个线程的唯一id都自增
                    //int i = atomicInteger.incrementAndGet();//调用官方的方法
                    int i = incrementAndGet(atomicInteger);//调用自己实现的的方法
                    System.out.println(i);
                }
            });
            thread.start();
        }
    }

    /*
     * 自己实现incrementAndGet方法,即当前值加1【线程安全】
     *  */

    public static int incrementAndGet(AtomicInteger var) {
        int expect, next;
        do {
            expect = var.get();
            next = expect + 1;
        } while (!var.compareAndSet(expect, next));
        /*
        * 对于上面的while:
        * 1、如果当前的值等于期待的值,那么返回true
        * 2、如果不等于,那么的返回false
        * */

        return next;
    }
}

 解释一下incrementAndGet方法的实现逻辑:

一开始定义int类型的两个变量:expect(预期值),next(下一个值)。在这个do...while循环中,循环体中先将当前内存中的var(也就是传入的AtomicInteger类型的automicInteger)值获取到给expect,再+1赋值给next。在循环条件中,调用的是AtomicInteger类的核心方法compareAndSet,这个方法顾名思义:先比较再赋值。因为有不同的多个线程存在,而每个线程都会先后要对var的值进行修改。如果一个线程A调用incrementAndGet时,预期值expect如果与当前var的值相等,才会将next赋值给var,假设线程A满足上面的要求,而同时如果在此时刻另外一个线程B也刚好调用这一方法时,由于var的值已经被线程A改变,这些线程的expect值就不会与比较时的var相等,所以它只能重新进入到循环中,期待下一次的相遇,但即使是走到最后也都是会找到自己的唯一。

 运行结果:

这里如果还要深挖compareAndSet的最底层,我们可以看到final native

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

它的底层是用c++实现的,这方面的源码可以自行查找资料。关乎到的就是操作系统方面的知识(汇编语言)。

可以这样说:compareAndSet内部是实现了原子性的,通过lock cmpxchgq实现了。(在java代码层面它是无锁的,在硬件层面还是有锁的,锁的颗粒度不同)

可能有人会有疑惑:为什么compareAndSet已经加了lock锁,为什么还会被其它线程抢走?

因为这里时乐观锁,它先获取预期值和设定下一个值,这一步是不加锁的,只有在比较交换时加锁的。(执行到最底层的那一步)

CAS无锁机制与ABA问题

1、原子性问题

lock cmpxchgq  缓存行锁/总线锁(这个就是一条原子命令,不会被其他线程打断执行或插入执行)

2、ABA问题

2.1、ABA问题的产生

    要了解什么是ABA问题,首先我们来通俗的看一下这个例子,一家火锅店为了生意推出了一个特别活动,凡是在五一期间的老用户凡是卡里余额小于20的,赠送10元,但是这种活动没人只可享受一次。然后火锅店的后台程序员小王开始工作了,很简单就用cas技术,先去用户卡里的余额,然后包装成AtomicInteger,写一个判断,开启10个线程,然后判断小于20的,一律加20,然后就很开心的交差了。可是过了一段时间,发现账面亏损的厉害,老板起先的预支是2000块,因为店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。小王一下就懵逼了,赶紧debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次!

阐述:

假设有个线程A去判断账户里的钱此时是15,满足条件,直接+20,这时候卡里余额是35.但是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,线程B去执行扫描账户的时候,发现它又小于20,又用过cas给它加了20,这样的话就相当于加了两次,这样循环往复肯定把老板的钱就坑没了!

本质:

ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。

2.2、AtomicStampReference 

AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:

//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);

public V getRerference();

public int getStamp();

public void set(V newReference,int newStamp);

2.3、AtomicStampReference的使用实例

我们定义了一个money值为19,然后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功,这就保证了每次只会被执行一次。

public class AtomicStampReferenceDemo {

    static AtomicStampedReference<Integer>  money =new AtomicStampedReference<Integer>(19,0);

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {

            int stamp = money.getStamp();

            System.out.println("stamp的值是"+stamp);

            new Thread(){         //充值线程

                @Override
                public void run() {

                        while (true){

                            Integer account = money.getReference();

                            if (account<20){

                                if (money.compareAndSet(account,account+20,stamp,stamp+1)){

                                    System.out.println("余额小于20元,充值成功,目前余额:"+money.getReference()+"元");
                                    break;
                                }
                            }else {

                                System.out.println("余额大于20元,无需充值");
                            }
                        }
                    }
                }.start();
            }


            new Thread(){

                @Override
                public void run() {    //消费线程

                    for (int j = 0; j < 100; j++) {

                        while (true){

                            int timeStamp = money.getStamp();//1

                            int currentMoney =money.getReference();//39

                            if (currentMoney>10){
                                System.out.println("当前账户余额大于10元");
                                if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){

                                    System.out.println("消费者成功消费10元,余额"+money.getReference());

                                    break;
                                }
                            }else {
                                System.out.println("没有足够的金额");

                                break;
                            }
                            try {
                                Thread.sleep(1000);
                            }catch (Exception ex){
                                ex.printStackTrace();
                                break;
                            }

                        }

                    }
                }
            }.start();

        }
    }

 这样实现了线程去充值和消费,通过stamp这个标记属性来记录cas每次设置值的操作,而下一次再cas操作时,由于期望的stamp与现有的stamp不一样,因此就会设值失败,从而杜绝了ABA问题的复现。

ABA问题摘自:探索CAS无锁技术 - Yrion - 博客园 (cnblogs.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值