首页 > 技术文章 > 【JUC】如何安全实现count++以及CAS为什么要出现?悲观、乐观锁又是啥?

cckong 2021-02-24 23:10 原文

我们实现一个例子。

我们有一个count变量。建立10个线程,每个线程都对count加1000次。(count++)

public class cas {
    static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads=new Thread[10];
        for (int i = 0; i < 10; i++) {

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        count++;
                    }
                    return;
                }
            });
            thread.start();
            threads[i]=thread;
        }
//        for(int i=0;i<10;i++)
//        {
//            threads[i].join();
//        }

        Thread.sleep(1000);//等所有线程完成计算
        System.out.println(count);
    }
}

 

 

 显而易见 答案并非我们所愿是10000.

这是因为count++这个操作在jvm引擎里执行步骤是:

(1)A=count

(2)B=A+1

(3)count=B

这样有可能有几个线程当拿到count时 还没进行加法 别的线程就已经加完了

(举个例子:

线程小明拿到count 线程小刚拿到count。此时count都为200

CPU对于小明照顾一点,让他先行完成了count++的运算 此时count变为201.

小刚拿来的count副本好好放在自己工作内存 不知道已经变了

所以还是老老实实做到count++将count变为201.此时小刚浪费了自己的一次++的机会。因为白加了)

 

 

 

 加了volatile更加离谱。想知道为什么 欢迎看我的博客:【JMM】java内存模型及volatile关键字底层 

 

还有一种方法就是原子类 AtomicInteger不过这次我们就搞了(想不搞的。。。)

public class cas {
//    static volatile int count=0;
    static AtomicInteger count=new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        count.set(0);
        Thread[] threads=new Thread[10];
        for (int i = 0; i < 10; i++) {

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        count.getAndIncrement();
                    }
                    return;
                }
            });
            thread.start();
            threads[i]=thread;
        }
//        for(int i=0;i<10;i++)
//        {
//            threads[i].join();
//        }

        Thread.sleep(1000);//等所有线程完成计算
        System.out.println(count);
    }
}

 

 

 

 那这必须没问题啊。

 

赶紧肝CAS吧。。。

我们回到那个三部曲

(1)A=count

(2)B=A+1

(3)count=B

如果对于整个方法加synchronized或者reentrantLockk非常慢啊 我们的线程小子们都变成了串行的了。这还能叫多线程?

我们对于第三步(3)进行修改以及升级

(1)A=count

(2)B=A+1

(3)1.获得锁

  2.获取count最新值new 

  3.CAS方法进行判断:是否当前count==A。相等则把B=count。不相等则循环等待相等的机会到来。

  4.释放锁

public class cas {
    static volatile int count=0;
    public static int getCount(){return count;}
    public static synchronized boolean compareAndSwap(int exceptCount,int now){
        if(getCount()==exceptCount)
        {
            count=now;
            return true;
        }
        else
            return false;
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        while(!compareAndSwap(count,count+1)){}//自旋
                    }
                    return;
                }
            });
            thread.start();
        }

        Thread.sleep(1000);//等所有线程完成计算
        System.out.println(count);
    }
}

 

 

 

 

 

自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

 

 

 

CAS的问题(ABA问题)

 

 

 

 

ABA问题的修改  对于比较的数据 变为一个pair 数据+版本号 

 

 

 

 

 

5.悲观锁、乐观锁

悲观锁指的就是我们平常使用的加锁机制,它假设我们总是处于最坏的情况下,如果不加锁数据完整性就会被破坏。

而乐观锁指是一种基于冲突检测的方法,检测到冲突时操作就会失败。

 

这个情绪是描述操作系统对于资源的。

比如互斥锁 就是悲观锁,操作系统对于这个资源是悲观的,认为只要多个线程请求它,肯定会出错。

比如CAS就是乐观锁 他其实和锁并没有关系 

 

6.原子类 atomicInteger的自增方法

 

 

 

推荐阅读