讲讲Java里计数器问题,对于理解原子性很有帮助。
线程安全的计数器 与 线程不安全的计数器
直接上代码,代码里实现了两种计数器,SafeCounter
和 NonSafeCounter
,顾名思义,前者是线程安全的,后者是线程不安全的。
线程安全的计数器内部使用了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(SafeCounter
和 NonSafeCounter
)去计数
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
来实现线程安全的计数器,大家肯定也很容易想到用synchronized
和Lock
上代码,SafeCounter_1
SafeCounter_2
SafeCounter_3
,分别使用了synchronized
,Lock
和AtomicLong
来实现线程安全的计数器
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:
参考:
线程安全并且无阻塞的Atomic类
浅析AtomicLong以及Unsafe
聊聊并发(五)——原子操作的实现原理
AtomicInteger源码分析——基于CAS的乐观锁实现
JAVA-CAS简介以及ABA问题
网友评论