述
在并发编程中,原子类也是经常使用的一个工具,利用原子类,可以把一些操作变成一个原子操作,在多线程的情况下不需要加锁也可以保证线程安全
原子类的作用及优势
原子类的作用跟锁是类似的,都是为了保证在并发环境下的线程安全,原子类相比于锁,有一定的优势
- 锁的粒度更细: 原子类可以把竞争范围缩小到变量级别,通常我们手动加锁的粒度都会大于原子变量的粒度
- 效率更高: 一般情况下,原子类的效率会比使用锁的效率高,除了高度竞争的情况
常用原子类
常用的原子类有以下几种
基本原子类型
Atomic*
:AtomicInteger
,AtomicLong
,AtomicBoolean
作用: 对基本类型的操作变为原子操作
原子数组类型
Atomic*Array
:AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
作用: 对数组的操作为原子操作
引用类型原子类
Atomic*Reference
:AtomicReference
,AtomicStampedReference
,AtomicMarkableReference
作用: 引用类型针对的都是对象
升级类型原子类
Atomic*FieldUpdater
:AtomicIntegerfieldupdater
,AtomicLongFieldUpdater
,AtomicReferenceFieldUpdater
作用: 将一个非原子变量转换为原子变量
累加器
Adder
:LongAdder
,DoubleAdder
Accumulator
:LongAccumulator
,DoubleAccumulator
作用: 高效累加操作
原子基本类型使用
以 AtomicInteger
为例,了解一下基本的用法,以及常用的方法
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
// 获取当前值
int i = atomicInteger.get();
// 获取当前值并设置新的值
int j = atomicInteger.getAndSet(1);
// 获取当前值并自增
int k = atomicInteger.getAndIncrement();
// 获取当前值并自减
int l = atomicInteger.getAndDecrement();
// 获取当前值,并加上指定的值
int m = atomicInteger.getAndAdd(5);
// 对比并设置值,如果当前的值为预期的值5,则以原子的方式将第二个值6设置为输入值
boolean b = atomicInteger.compareAndSet(5, 6);
}
}
原子数组
原子数组就是数组中每个属性都是原子的,下面代码执行100次的加减操作,最后Array中的每个属性结果都为0
public class AtomicArrayDemo {
public static void main(String[] args) throws InterruptedException {
//原子数组 长度1000
AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(1000);
Increment increment=new Increment(atomicIntegerArray);
Decrementer decrementer=new Decrementer(atomicIntegerArray);
//启动100个线程 进行数组的加减
Thread[] threadsIncrementer=new Thread[100];
Thread[] threadsDecrementer=new Thread[100];
for (int i =0;i<100;i++){
threadsDecrementer[i]=new Thread(decrementer);
threadsIncrementer[i]=new Thread(increment);
threadsDecrementer[i].start();
threadsIncrementer[i].start();
}
for (int i =0;i<100;i++){
threadsDecrementer[i].join();
threadsIncrementer[i].join();
}
//判断加减之后值是否非零
for (int i=0;i<atomicIntegerArray.length();i++){
if (atomicIntegerArray.get(i)!=0){
System.out.println("发现了错误"+i);
}
}
System.out.println("运行结束");
}
}
class Decrementer implements Runnable{
private AtomicIntegerArray array;
public Decrementer(AtomicIntegerArray array){
this.array=array;
}
@Override
public void run() {
for(int i =0;i<array.length();i++){
//i是数组下标 每个值都自减
array.getAndDecrement(i);
}
}
}
class Increment implements Runnable{
private AtomicIntegerArray array;
public Increment(AtomicIntegerArray array){
this.array=array;
}
@Override
public void run() {
for(int i =0;i<array.length();i++){
//i是数组下标 每个值都自加
array.getAndIncrement(i);
}
}
}
引用原子类型介绍
AtomicReference
这个类的作用,和 AtomicInteger
本质上没有区别, AtomicInteger
是让一个整形变量保证原子性, AtomicReference
可以让一个对象保证原子性 ,不过一个对象是可以包含很多属性的
在之前锁的章节,有一个自旋锁的案例,已经用到这个类型了, 代码再贴过来回顾一下
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
private void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
}
}
private void unLock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
}
这里就用到了一个原子类的 compareAndSet
这个CAS操作,对比并赋值
升级类型原子类的使用
升级类型原子类,功能是把一个普通的变量升级为原子类,主要的使用场景是当一个普通变量偶尔需要原子操作的时候使用
以 AtomicIntegerfieldupdater
为例,就是将一个整型升级为原子整型
public class AtomicIntegerFieldUpdaterDemo implements Runnable {
static Candidate tom;
static Candidate peter;
public static AtomicIntegerFieldUpdater<Candidate> soureUpdater=
AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
@Override
public void run() {
for (int i=0;i<10000;i++){
peter.score++;
soureUpdater.getAndIncrement(tom);
}
}
public static class Candidate{
volatile int score;
}
public static void main(String[] args) throws InterruptedException {
tom=new Candidate();
peter=new Candidate();
AtomicIntegerFieldUpdaterDemo r=new AtomicIntegerFieldUpdaterDemo();
Thread t1=new Thread(r);
Thread t2=new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(peter.score);
System.out.println(tom.score);
}
}
这里就会把属性tom
这个对象的 score
变为原子操作 。
注意点:变量必须是可见的,也就是必须使用volatile
修饰;不能使用static
修饰;
Adder累加器
在高并发下 LongAdder
的效率比 AtomicLong
要高 ,以空间换时间。
使用方式就是 new LongAdder();
然后调用 add()
方法就可以了
那么为什么 AtomicLong
的性能不如 LongAdder
呢?
AtomicLong
每次执行修改操作,都要 flush
到主存 和 refresh
其他线程的工作内存,在高并发的情况下效率就很低了,而 LongAdder
每个线程各自维护自己的变量以及值,用来在自己的线程内计算,等最后要获取值的时候,把所有线程的和在加起来就是总和
LongAdder 带来的设计和原理
LongAdder
引入了分段累加的概念,内部有一个base变量和一个Cell[]数组参与计算;当竞争不激烈的时候,直接累加到base变量,竞争激烈的时候,各个线程分散累加到自己的槽Cell[i]中。
对比 AtomicLong
在低竞争下, AtomicLong
和 LongAdder
这两个类具有相似的特征,但是在竞争激烈的情况下,LongAdder
的预期吞吐量要高得多,但要消耗更多的空间。并且最后的求和不是十分十分的精确,比如,当最后的求和加到一半的时候,Cell数组的前半部分又有值改变了,就会影响最终结果。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
LongAdder
适合的场景是统计求和计数的场景,而且 LongAdder
基本只提供了 add 方法,而 AtomicLong
还具有cas方法
Accumulator 累加器
Accumulator
是一个更加灵活的累加器,使用方式上跟上面的 LongAdder
是有些不同的,如下
LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);
LongAccumulator accumulator1 = new LongAccumulator((x, y) -> x + y, 0);
accumulator1.accumulate(1);
// 获取后清空
accumulator1.getThenReset();
代码中的 accumulator
和 accumulator1
作用都是一样的, 就是lambda的两种写法,下面这个看着更直观一点
LongAccumulator
需要两个参数,第一个是 LongBinaryOperator
的实现它是一个函数式接口, 然后传参的时候传具体的实现就可以,第二个参数 0 就是 x 的初始值
上面代码中,刚初始化的时候 x 就是 0 ,然后第一次累加的时候会把x的值给y,然后再把传进来的x跟y做累加,结果赋值给 y
当然这个表达式不一定就是累加,也可以做自己的实现,所以这个比上面的 LongAdder
要更加灵活
适用场景
1.需要大量计算,可以用多线程并行执行
2.对计算顺序不能有要求,线程1可能在线程5之后执行也可能之前执行,不影响最后结果