1. 代码中的加法
1+1=2,这是一个数学里普通的加法操作,那么使用代码进行展现的话如下所示
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
使用的时候需要先进行对象实例化,然后调用方法。这没什么问题,但是
当多线程的时候,情况可能变的不一样,我创建5个线程同时对Counter对象进行调用increment()
方法1000次
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
Counter counter = new Counter();
for (int i = 0; i < 5; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
counter.increment();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(counter.value());
}
得到的结果是4960,而且每一次执行的结果还不一样,这当中发生了什么?
答案是 多个线程间没有保证内存操作的原子性。
2. 原子操作
要弄懂上面说的答案 ,我们先需要知道,什么是原子操作。
Atomic,英文翻译为原子的,发音为 [əˈtɒmɪk]。
原子(atom)本意是不能进一步分割的最小粒子,那么扩展一下,原子操作,那么就意味着“不可被中断的一个或者一系列操作”。
先来看一个不是原子性操作的例子,如果i=1,我们进行两次i++操作,我们的期望的结果是3,但是结果有可能是2。
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行+1操作,然后分别写入系统内存中。
怎么解决?聪明的你肯定想到了,给方法上加上synchronized
,像这样
class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
或者使用显示锁
public class Counter {
private int value;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
value++;
lock.unlock();
}
public void decrement() {
lock.lock();
value--;
lock.unlock();
}
public synchronized int get() {
return value;
}
}
当然没有问题,但是对于更复杂的类,我们可能要避免不必要的同步对活动性的影响。用AtomicInteger替换int字段使我们能够防止线程干扰而无需求助于同步。
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
3. Java中的Atomic
在Java的java.util.concurrent.atomic包下,有17个类,其中
基本类:AtomicInteger AtomicLong AtomicBoolean
引用类型:AtomicReference AtomicStampedReference AtomicMarkableReference
数组类型:AtomicIntegerArray AtomicLongArray AtomicReferenceArray
属性原子修改器:AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater
可以通过原子的方式更新对应的值。
上面提到的是12个,自Java8以来引入了4个新的计数器类型 DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
那么在LongAdder 与AtomicLong有什么区别?
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
它们都是怎么做到原子操作的?
3.1 原理
我们点进AtomicInteger源码进行查看
它的值是存在一个volatile的int里面。volatile只能保证这个变量的可见性。不能保证他的原子性。
看看getAndIncrement
这个类似i++的函数,可以发现,是调用了UnSafe中的getAndAddInt
。
那他如何保证其原子性呢?
自旋 + CAS(乐观锁)。在这个过程中,通过compareAndSwapInt
比较更新value值,如果更新失败,重新获取旧值,然后更新。
AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中的value内存地址,从而可以根据CAS实现对value字段的原子操作
3.2 实际应用
对于这个Atomic,在实际应用中,可以完成哪些功能?
3.2.1 多线程安全的全局唯一ID生成器
class IdGenerator {
AtomicLong var = new AtomicLong(0);
public long getNextId() {
return var.incrementAndGet();
}
}
3.2.2 限制并发流量,用来限制接口的流量,超过并发的数量的阈值进行熔断
3.3 Atomic优缺点
优点: CAS相对于其他锁,不会进行内核态操作,有着一些性能的提升
缺点: 引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu资源会消耗很高。
参考文章:
Atomic Variables
Understanding Atomic Variables in Java
使用Atomic
Java并发编程包中atomic的实现原理