原子类
原子类是指JUC库里的atomic包下的类。
这些原子类都具有原子性(在JMM文章中有详细叙述)
提到原子,就会想到化学,在高中化学所学的相关的一些化学方程式中,原子是不可再分的(这里并不去探究中子、质子、夸克什么)
而我们所谓的原子性就是这个性质:不可再分
原子性可以保障线程安全,因为是不可再分,所以在同一时间,只能有一个线程能够正确操作,达到了线程安全的效果
相对于锁而言,原子类的粒度更细,锁保证的原子性的粒度通常一个临界区,有多个代码,而原子类可能将这个范围缩小的一个变量。
所以,原子类的性能也要优于锁(非高度竞争情况下)
原子基本类型
Java提供了三个基本类型的原子类:
AtomicBoolean、AtomicInteger、AtomicLong
下面就用AtomicInteger举例
常用方法:
- public final int get() //获取当前值
- public final int getAndSet(int newValue) //获取当前值并设置为新值
- public final int getAndAdd(int delta) //获取当前值并增加设定的值
- public final int getAndIncrement() //获取当前值并自增
- public final int getAndDecrement() //获取当前值并自减
- public final boolean compareAndSet(int expect,int update) //如果当前值是expect,则将该值原子设置为update。
//true表示成功,false表示当前值不等于expect
使用getAndIncrement()和普通基本类型的++操作进行对比
public class AtomicIntegerDemo {
private static AtomicInteger atomicInt = new AtomicInteger(0);
private static volatile int normalInt = 0;
private static void addAtomicInt(){
atomicInt.getAndIncrement();
}
private static void addNormalInt(){
normalInt++;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int i = 0; i <5000; i++) {
addAtomicInt();
addNormalInt();
}
};
new Thread(r).start();
new Thread(r).start();
Thread.sleep(100);
System.out.println("原子基本类型:"+atomicInt+" 基本类型:"+normalInt);
}
}
打印结果:
原子基本类型:10000 基本类型:9780
原子数组类型
Java提供了三个数组类型的原子类:
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
在数组内,每个数组元素都是具有原子性的
下面同样用AtomicIntegerArray来举例
public class AtomicArrayDemo {
//创建一个1000容量的原子整型数组
private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(1000);
//每个元素自减1
private static void decre(AtomicIntegerArray atomicIntegerArray){
for (int i = 0; i <atomicIntegerArray.length(); i++) {
atomicIntegerArray.getAndDecrement(i);
}
}
//每个元素自增2
private static void add(AtomicIntegerArray atomicIntegerArray){
for (int i = 0; i <atomicIntegerArray.length(); i++) {
atomicIntegerArray.getAndAdd(i,2);
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
threadPool.execute(()->add(atomicIntegerArray));
threadPool.execute(()->decre(atomicIntegerArray));
threadPool.shutdown();
while (true){
if (threadPool.isTerminated()){
for (int i = 0; i <atomicIntegerArray.length(); i++) {
if (atomicIntegerArray.get(i)!=1){
System.out.println("出现错误");
}
}
System.out.println("完成");
break;
}
}
}
}
用100个线程的线程池来并发对数组每个元素进行操作,如果有一个数组的结果为1(初始为0,执行-1,+2操作),就打印出出现错误
,但是由于原子性,并不会出现这种结果。
原子引用类型
Java提供了三个引用类型的原子类:
AtomicReference、AtomicMarkableReference、AtomicStampedReference
AtomicInteger和AtomicReference本质并无多少区别,不过前者是让一个整型保证原子性,而后者是让一个对象保证原子性。
由于对象的内部比整型更丰富,所以AtomicReference的功能也需要更强大,但是使用并无多少差别
在上一章锁的文章中,关于自旋锁的介绍时,我们使用到了这个类来实现了一把自旋锁
普通变量升级原子类型
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
这三个类的作用是,将普通的变量,升级为具有原子性的变量。
有些时候,一个已经由普通变量定义好了的程序,需要增加一个方法,这个方法会遇到并发的场景,此时我们肯定不能全部推倒重来,这时就是使用这个类的时候。
同样,一个程序,只有少数时候需要原子性操作,其他时候并不需要考虑原子性,那么我们可以用普通的变量来节约操作成本,只需要在原子性操作时进行升级即可。
用升级前后的变量的自增操作来对比.
public class AtomicIntegerFieldUpdaterDemo {
private volatile int a;
//普通变量
private static AtomicIntegerFieldUpdaterDemo normalDemo = new AtomicIntegerFieldUpdaterDemo();
//升级变量
private static AtomicIntegerFieldUpdaterDemo updaterDemo = new AtomicIntegerFieldUpdaterDemo();
//获取updater
private static AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterDemo> updater = AtomicIntegerFieldUpdater
.newUpdater(AtomicIntegerFieldUpdaterDemo.class,"a");
//两个变量都进行自增操作
private static void increment(){
for (int i = 0; i <5000; i++) {
normalDemo.a++;
updater.getAndIncrement(updaterDemo);
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(()->increment()).start();
new Thread(()->increment()).start();
Thread.sleep(500);
System.out.println("普通变量: "+normalDemo.a);
System.out.println("升级变量: "+updaterDemo.a);
}
}
普通变量: 9887
升级变量: 10000
值得注意的是,我们在调用AtomicIntegerFieldUpdater方法时,传递了两个参数,类名和变量名,很明显是通过反射调用的。
同样,该方法不支持static变量,会抛出初始化异常错误。
累加器Adder
JUC中提供了两个累加器:DoubleAdder、LongAdder
累加器是JDK1.8后引入的,相比如普通的原子类,在多线程的情况下,普通原子类带来了性能上的瓶颈,而累加器在并发场景下的效率要更高,本质是通过空间换时间。
下面来对比高并发场景下AtomicLong和LongAdder的性能:
public class LongAdderDemo {
private static AtomicLong atomicLong = new AtomicLong();
private static LongAdder longAdder = new LongAdder();
private static void atomicLongIncrement(){
for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
atomicLong.getAndIncrement();
}
}
private static void longAdderIncrement(){
for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
longAdder.increment();
}
}
private static long runTime(Runnable runnable){
ExecutorService threadPool = Executors.newFixedThreadPool(16);
long Start = System.currentTimeMillis();
for (int i = 0; i <Integer.MAX_VALUE>>>16; i++) {
threadPool.submit(runnable);
}
threadPool.shutdown();
while (!threadPool.isTerminated()){
}
long End = System.currentTimeMillis();
return End-Start;
}
public static void main(String[] args) {
System.out.println("AtomicLong 耗时:"+runTime(()->atomicLongIncrement())+" 结果:"+atomicLong.get());
System.out.println("LongAdder 耗时:"+runTime(()->longAdderIncrement())+" 结果:"+longAdder.sum());
}
}
AtomicLong 耗时:23627 结果:1073676289
LongAdder 耗时:4453 结果:1073676289
可以看到性能差距很明显。
为什么会出现这样的性能差异呢
这就和Java内存模型有关了
在JMM文章中关于谈到JMM的共享内存和本地内存时,也提到了计算机底层硬件的一些知识。
其中包括了冲刷处理器缓存和刷新处理器缓存
AtomicLong多线程情况下的自增就是基于这两个操作。
线程1每次进行自增操作时,需要从共享内存中读取变量到本地内存,也就是refresh操作。
当线程1执行完自增后,需要把本地内存的变量写回共享内存,也就是flush操作。
同理其他线程也是如此
所以每次操作都需要进行同步,在高并发场景下,冲突带来了性能的降低
而LongAdder则不一样,每个线程在自己的本地内存有一个变量,比如线程1有val1,线程2有val2,线程不去共享内存读取和刷新,只在自己的工作内存对自己的val进行自增操作。
LongAdder采取的是分段累加的策略
类似JDK1.7 ConcurrentHashMap里分段锁的概念
这也是最后调用的是LongAdder的sum方法:
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;
}
里面有两个值得注意的点:Cell 和 base
- base变量:如果竞争不激烈,就直接累加在这个变量上
- Cell[]:如果竞争激烈,每个线程分段累加在自己所占的数组槽上,通过hash将每个线程分别对应一个槽,不会对其他线程造成干扰。
总结:
在低争用场景下,AtomicLong和LongAdder差别并不大。当并发量高了时,LongAddert用用空间换时间,提升了性能,但是多消耗了空间。
同时,LongAdder使用场景稍局限,通常适用于计数、求和方面,提供的都是加减等操作。而AtomicLong功能更丰富,还具有cas方法。
CAS
CAS(Compare and Swap)是一种算法思想,也是乐观锁的实现方式。同时这也是一个CPU指令。上面的原子类就用到了CAS操作。
CAS是用于并发场景的,常见于原子类、并发容器。
它只关心三个值:内存值V,预期值A,修改值B
首先它会去比较内存中的V和我所预期的值A是否相等,如果相等,就修改为B。否则就不操作。
CAS指令由CPU保证了对共享变量操作的原子性,但并没有保证可见性。
CAS是一个“if-then act”操作,我们可以通过代码来模拟这一指令
public class CAS {
private int value;
public synchronized int compareAndSwap(int except,int update){
if (except==value){
value=update;
}
return value;
}
}
Java是如何使用到CAS来实现原子操作的呢?
Java提供了一个Unsafe
类,来直接操作内存数据
该类是CAS的核心类,提供了硬件级别的原子操作
下面用AtomicInteger为例
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;//变量在内存中的偏移地址
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));//通过反射获取value值的内存偏移地址
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;//volatie保证可见性
CAS并不是尽善尽美,它也有缺点。
- ABA问题
- 自旋问题
因为CAS是对值进行比较
如果线程1将值从0改为1,
线程2又把值改回0。
第三个线程过来发现值是0,会认为没有线程进行修改过,在后续的操作可能会出现错误。
同样,有时CAS的操作是需要长时间自旋操作,在自旋锁的实现时我们就通过自旋CAS操作来实现,而长时间的空自旋会导致CPU资源的浪费。