计数器

讲讲Java里计数器问题,对于理解原子性很有帮助。

线程安全的计数器 与 线程不安全的计数器

直接上代码,代码里实现了两种计数器,SafeCounterNonSafeCounter,顾名思义,前者是线程安全的,后者是线程不安全的。
线程安全的计数器内部使用了AtomicLong,它的自增操作是原子性的。
而线程不安全的计数器直接使用了Long,它连单次读,或单次写,都不是原子性的(加上volatile关键字可使得单次读,或单次写具有原子性,同样情况的还有Double),那就更别提自增了(自增相当于一次读加一次写)

class NonSafeCounter{    
    private long count = 0;    
    public void increase()    
    {    
        count++;    
    }    
    
    public long  get()    
    {    
        return count;    
    }    
}    
    
class SafeCounter{    
    private AtomicLong atomicLong  = new AtomicLong(0);
    public void increase()    
    {    
        atomicLong.incrementAndGet();
    }    
    
    public long get()    
    {    
        return atomicLong.longValue();    
    }    
} 

主函数无非就是多线程去使用Counter(SafeCounterNonSafeCounter)去计数

public class CounterTest {    
    
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException    
    {    
        final int loopcount = 10000;    
        int threadcount = 10;    
        //Non Safe
        final NonSafeCounter nonSafeCounter = new NonSafeCounter();    
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadcount + 1);
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        nonSafeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("NonSafeCounter:" + nonSafeCounter.get());    
    
        
        //Safe
        final SafeCounter safeCounter = new SafeCounter();    
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        safeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("SafeCounter:" + safeCounter.get());    
    }    
}    

最后的打印结果:

Thread Finished: 8
Thread Finished: 1
Thread Finished: 2
Thread Finished: 7
Thread Finished: 6
Thread Finished: 0
Thread Finished: 5
Thread Finished: 9
Thread Finished: 3
Thread Finished: 4
NonSafeCounter:39180
Thread Finished: 8
Thread Finished: 2
Thread Finished: 4
Thread Finished: 6
Thread Finished: 1
Thread Finished: 5
Thread Finished: 9
Thread Finished: 3
Thread Finished: 7
Thread Finished: 0
SafeCounter:100000

可以看出,多线程情况下,必须要使用一些同步策略(此处是AtomicLong)来保证计数器的正确性。

加个volatile试试

上面说到了,volatile不能保证自增操作(如count++)的原子性,还是试验下,给NonSafeCounter加上volatile,然后重新运行

class NonSafeCounter{    
    private volatile long count = 0;    
    public void increase()    
    {    
        count++;    
    }    
    
    public long  get()    
    {    
        return count;    
    }    
}  

输出:

Thread Finished: 8
Thread Finished: 1
Thread Finished: 7
Thread Finished: 6
Thread Finished: 0
Thread Finished: 2
Thread Finished: 9
Thread Finished: 3
Thread Finished: 4
Thread Finished: 5
NonSafeCounter:49017
Thread Finished: 8
Thread Finished: 2
Thread Finished: 1
Thread Finished: 0
Thread Finished: 3
Thread Finished: 6
Thread Finished: 9
Thread Finished: 4
Thread Finished: 7
Thread Finished: 5
SafeCounter:100000

这个输出说明了,我没有骗大家,volatile不能保证自增操作的原子性。
但比较有趣的时,多跑几次代码你会发现,加了volatile关键字,最后count出来的值总是大于没加volatile关键字(虽然都不正确)的时候。我觉得一个合理的解释是,volatile保证读写都在主存上(可见性),而没加volatile时,多个线程在做自增操作时是在cpu的寄存器里,这样自然漏加很多。
到这里,我觉得引出了两个问题:

  • 线程安全的计数器,还有其它的实现吗?不同实现有什么区别?
  • AtomicLong 如何保证自增操作的原子性?

线程安全计数器 不同实现

先来说说上面提到的第一个问题
除了用AtomicLong来实现线程安全的计数器,大家肯定也很容易想到用synchronizedLock
上代码,SafeCounter_1 SafeCounter_2 SafeCounter_3,分别使用了synchronizedLockAtomicLong 来实现线程安全的计数器

interface SafeCounterI{
    public void increase();  
    public long get();
}
class SafeCounter_1 implements SafeCounterI{    
    private long count = 0;    
    public synchronized void increase()    
    {    
        count++;    
    }    
    public long  get()    
    {    
        return count;    
    }    
}
class SafeCounter_2 implements SafeCounterI{    
    private long count = 0;
    Lock lock = new ReentrantLock();
    public void increase()    
    {   
        try{
            lock.lock();
            count++;    
        }finally{
            lock.unlock();
        }
    }    
    public long  get()    
    {    
        return count;    
    }    
}
class SafeCounter_3 implements SafeCounterI{    
    private AtomicLong atomicLong  = new AtomicLong(0);
    public void increase()    
    {    
        atomicLong.incrementAndGet();
    }    
    public long get()    
    {    
        return atomicLong.longValue();    
    }    
}

为了测试三种不同实现的性能好坏,加上程序运行的时间

    public static void main(String[] args) throws Exception    
    {   
        Long start = System.currentTimeMillis();
        final SafeCounterI safeCounter= new SafeCounter_1();  
        multiThreadCount(safeCounter);
        System.out.println(System.currentTimeMillis() - start);
        
    }

multiThreadCount(safeCounter)是多线程去计数的逻辑,为了能直观的体现出性能的好坏,把单个线程count的数量加到了100000(final int loopcount = 100000),线程数加到了100(int threadcount = 100)
Thread.sleep(300);是为了让Main Thread在其它线程都完全返回后再执行。

    private static void multiThreadCount(final SafeCounterI safeCounter)
            throws InterruptedException, BrokenBarrierException {
        final int loopcount = 100000;    
        int threadcount = 100;    
        //Non Safe
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadcount + 1);
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        safeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("NonSafeCounter:" + safeCounter.get());
    }

好了,在我的环境上,使用SafeCounter_1,多次运行,发现执行时间基本在870ms - 920ms这个区间

...
Thread Finished: 95
Thread Finished: 81
NonSafeCounter:10000000
884

使用SafeCounter_2,运行时间基本在620ms - 650ms这个区间

Thread Finished: 66
Thread Finished: 35
NonSafeCounter:10000000
638

而使用SafeCounter_3,运行时间基本在460ms - 500ms这个区间

Thread Finished: 39
Thread Finished: 42
NonSafeCounter:10000000
478

那结论就出来了,性能上AtomicLong 好于 Lock 好于 synchronized
那为什么AtomicLong性能好?
同样,还有之前的问题:AtomicLong 如何保证自增操作的原子性?

AtomicLong

前面我们看到,用AtomicLong来实现计数器时,调用了方法atomicLong.incrementAndGet(),这个方法做的就是一个自增操作,而且这个方法是原子性的,它如何做到的呢?网上看到incrementAndGet()的源码,虽然应该是AtomicInteger的代码,但思想应该一样:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
  • AtomicLong的各种操作,通过CAS来保证原子性:
    compareAndSet(current, next)即是CAS了,简单的说,它通过比较前值是否和内存中一样,来决定是否更新。
    前面说到了自增包括一次读,一次写,这里先“取原值”(int current = get()),然后“计算”(int next = current + 1),照理说接下来就该写了。但多线程环境下谁也无法保证在"取原值"和"计算"期间是否有其它线程已对“原值”做出了修改,那怎么办?
    CAS通过比较之前取出的“原值”和内存中的实际值,来确定是否有来自其它线程的更新,如果相同就说明没有其它线程的更新,那接着就写入。如果不相同,那简单,你重新跑次。
  • AtomicLong 通过乐观锁的方式,使得性能更好
    其实上面这种CAS加循环的方式就实现了一个“乐观锁”,相比“悲观锁”的实现(Lock synchronized),“乐观锁”认为线程冲突总是少的,如果有冲突我就回退重跑,那这样就节省了“悲观锁”里线程间竞争锁的开销。

我们都知道,cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换。切换涉及到清空寄存器,缓存数据。然后重新加载新的thread所需数据。当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notify(),notifyAll()唤醒回来。在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度。这就是典型的悲观锁的实现。独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

但是,由于在进程挂起和恢复执行过程中存在着很大的开销。当一个线程正在等待锁时,它不能做任何事,所以悲观锁有很大的缺点。举个例子,如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高。

所以就有了乐观锁的概念,他的核心思路就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直while循环,如果失败就重试,直到成功为止。所以,当数据争用不严重时,乐观锁效果更好。比如CAS就是一种乐观锁思想的应用。

ABA问题

CAS看似不错,但也有自己的问题,那就是ABA问题。
简单的说就是,在1号线程“取原值”和“CAS操作”中间,2号线程把“原值”A改为B,然后又从B改为A,那1号线程在接着做“CAS操作”时,发现内存中还是A,就继续做下去。然而此时已违反了原子性。
解决这个问题的方法其实也很简单,带个版本修改信息。
Java CAS 和ABA问题

关键字

  • AtomicLong
  • Lock
  • synchronized
  • volatile
  • CAS
  • ABA (加version解决)
  • 悲观锁 乐观锁

Code:

Sample Code on Github

参考:

线程安全并且无阻塞的Atomic类
浅析AtomicLong以及Unsafe
聊聊并发(五)——原子操作的实现原理
AtomicInteger源码分析——基于CAS的乐观锁实现
JAVA-CAS简介以及ABA问题

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

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,699评论 0 11
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,235评论 11 349
  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,815评论 1 19
  • 一个计数器通常是由一组触发器构成,该组触发器按照预先给定的顺序改变其状态,如果所有触发器的状态改变是在同一时钟脉冲...
    锦穗阅读 13,285评论 0 6
  • “邵子牙贡丸.鱼丸”是我在鼓浪屿上遇到的最满意的一家店了。 由于避开了端午的高峰,所以当我从龙头路上溜溜达达走到这...
    青年西米阅读 5,560评论 7 18