计数器

作者: 昵称全尼马被注册了 | 来源:发表于2017-03-05 19:28 被阅读0次

讲讲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问题

相关文章

  • jmeter计数器的使用

    1、计数器简介 2、添加计数器 3、计数器界面如下: 1(starting value):给定计数器的起始值、初始...

  • CountDownLatch

    计数器,指定计数器的大小,当执行latch.countDown()后计数器减一,当计数器等于0时才能执行latch...

  • 【2019-05-09】MapReduce的特性

    计数器内置计数器 任务计数器采集任务的相关信息,每个作业的所有任务的结果会被聚集起来。任务计数器由其关联任务维护,...

  • MapReduce(6)特性

    ##08.20 1、计数器 1.1、MapReduce包含的高级特性,计数器,数据集的排序和连接 1.2、计数器作...

  • CSS进阶八:counter(),box-sizing,appe

    css中的计数器 counter-reset:name number 设置或重置一个计数器 name 计数器名称 ...

  • 命题-十三章-填空

    定时/计数器的计数方式_____ 答案 可预置数,加法计数器 定时/计数器预置数的计算方法____ 答案 最大...

  • Java虚拟机的运行时数据区域

    Java虚拟机的运行时数据区域有程序计数器、虚拟机栈、本地方法栈、堆和方法区。 程序计数器 程序计数器,程序计数器...

  • Java CountDownLatch

    CountDownLatch 通过初始化一个计数器,通过countDown减少计数器,当计数器变成0时,才开始执行...

  • KubeEdge官方示例运行成功_Counter Demo 计数

    运行KubeEdge官方示例_Counter Demo 计数器 KubeEdge Counter Demo 计数器...

  • jmeter中计数器的使用

    配置元件-计数器(counter) 定义: 允许用户在线程组中任何位置创建引用的计数器。计数器允许用户配置起始点,...

网友评论

    本文标题:计数器

    本文链接:https://www.haomeiwen.com/subject/updegttx.html