通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

一、实现网站访问计数器

1、线程不安全的做法

1.1、代码

package com.chentongwei.concurrency; import static java.lang.Thread.sleep; /** * @Description:
 * @Project concurrency */
public class TestCount { private static int count; public void incrCount() {
        count ++;
    } public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount(); // 开启五个线程
        for (int i = 0; i < 5; i++) { new Thread(() -> { try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } // 每个线程都让count自增100
                for (int j = 0; j < 100; j++) {
                    testCount.incrCount();
                }
            }).start();
        }
        sleep(2000); // 正确的情况下会输出500
 System.out.println(count);
    }
}

1.2、结果

并不一定是500,极大可能小于500。不固定。

1.3、分析

很明显上面那段程序是线程不安全的,为什么线程不安全?因为++操作其实是类似如下的两步骤,如下:

count ++; ||
// 获取count
int temp = count; // 自增count
count = temp + 1;

很明显是先获取在自增,那么问题来了,我线程A和线程B都读取到了int temp = count;这一步,然后都进行了自增操作,其实这时候就错了因为这时候count丢了1,并发了。所以导致了线程不安全,结果小于等于500。

2、Synchronized保证线程安全

2.1、代码

package com.chentongwei.concurrency; import static java.lang.Thread.sleep; /** * @Description:
 * @Project concurrency */
public class TestCount { private static int count; public  void incrCount() {
        count ++;
    } public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount(); for (int i = 0; i < 5; i++) { new Thread(() -> { try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } for (int j = 0; j < 100; j++) { synchronized (TestCount.class) {
                        testCount.incrCount();
                    }
                }
            }).start();
        }
        sleep(2000);
        System.out.println(count);
    }
}

2.2、结果

500

2.3、分析

没什么可分析的,我用了Java的内置锁Synchronized来保证了线程安全性。加了同步锁之后,count自增的操作变成了原子性操作,所以最终输出一定是500。众所周知性能不好,所以继续往下看替代方案。

3、原子类保证线程安全

3.1、代码

package com.chentongwei.concurrency; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.Thread.sleep; /** * @Description:
 * @Project concurrency */
public class TestCount { // 原子类
    private static AtomicInteger count = new AtomicInteger(); public  void incrCount() {
        count.getAndIncrement();
    } public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount(); for (int i = 0; i < 5; i++) { new Thread(() -> { try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } for (int j = 0; j < 100; j++) {
                    testCount.incrCount();
                }
            }).start();
        }
        sleep(2000);
        System.out.println(count);
    }
}

3.2、结果

500

3.3、分析

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。每个原子类内部都采取了CAS算法来保证的线程安全性。

二、什么是CAS算法

1、概念

CAS的英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

2、原理

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,才将内存值修改为B,否则什么都不做,最后返回现在的V值。

简单理解为这句话:我认为V的值应该是A,如果是A的话我就把他改成B,如果不是A的话(那就证明被别人修改过了),那我就不修改了,避免多人 同时修改导致数据出错。换句话说:要想修改成功,必须保证A和V中的值是一样的,修改前有个对比的过程。

比如:更新一个变量,只有当变量的预期值(A)和内存地址(V)的实际值相同时,才会将内存地址(V)对应的值修改为B。

我们看如下的原理图:

1、在内存地址V当中,存储着值为10的变量。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

2、此时线程1想把变量的值增加1,对于线程1来说,旧的预期值A=10,要修改的新值B=11。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

3、在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量率先更新成了11。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

4、线程1开始提交更新,首先进行A和地址V的实际值对比,发现A!=V,提交失败。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

5、线程1重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说:A=11,B=12.这个重新尝试的过程称为自旋

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

6、这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

7、线程1进行交换,把地址V的值替换为B,也就是12.

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

3、对比Synchronized

从思想上来讲,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,高并发情况下效率低下。而CAS属于乐观锁,乐观的认为程序中的并发情况不那么严重,所以让线程不断去重试更新。但实际上Synchronized已经改造了,带有锁升级的功能。效率不亚于cas。

4、CAS缺点

(1)CPU开销可能过大

在并发比较大的时候,若多线程反复尝试更新某个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。(因为是个死循环,下面分析底层实现就懂了。)

(2)不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子操作,而不能保证整个代码块的原子性。比如需要保证三个变量共同进行原子性的更新,就不得不使用Synchronized或Lock等机制了。

(3)ABA问题。

下面会单独抽出一块地来详细讲解。这是CAS最大的漏洞。

三、CAS底层实现(Java)

1、概述

要说Java中CAS的案例,那么最属java.util.concurrent.atomic包下的原子类有发言权了。最经典、最简单。

2、讲解

比如我们这里随便找个AtomicInteger来讲解CAS算法底层实现。

public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next;
    }
} private volatile int value; public final int get() { return value;
}
  1. 获取当前值

  2. 当前值+1,计算出目标值

  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性)。

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。

什么是unsafe呢?

3、Unsafe

Unsafe是CAS的核心类,Java语言不像C,C++那样可以直接访问底层操作系统,Java无法直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作

而valueOffset是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

四、ABA问题

1、演示

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A。然后线程1执行CAS时发现变量的值仍是A,所以CAS成功,这么看没毛病,但是如果操作的是个链表呢?那就炸了,因为虽然值一样,但是链表的位置不一样了。

例如:

(1)现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

(2)在T1执行上面这条指令(CAS)之前,线程T2介入,将A、B出栈,在push三个D、C、A,如下:

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

(3)此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,因为B已经再上一步被移除了,成为了游离态。所以此时的情况变为

通过实现网站访问计数器带你理解 轻量级锁CAS原理,还学不会算我输!!!

导致了其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

2、生活案例

你和你前任分手后她又回来了,但是你在这期间又和其他女人...,你表面还是你,但是本质的你已经变了。把这个例子带到代码里来就是:

你有个class,里面有个LinkedList属性,这个链表里有你和你前任,你先把它踹了,然后小苍进来跟你...,这时候你前任就回来了,但是这期间链表已经发生了无感知的变化。`

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353