1 Atomic原子类简介
原子(atomic)本意是“不能被进一步宰割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
即便是在多个线程一起执行的时候,一个操作一旦开始,就不会被其余线程烦扰。简而言之,原子类就是具备原子/原子操作特色的类,它们能无锁地防止原子性问题。
并发包 java.util.concurrent
的原子类都寄存在java.util.concurrent.atomic
下,如下图所示。
依据操作的数据类型,能够将JUC包中的原子类分为次要5类:
(1)根本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
(2)数组类型
- AtomicIntegerArray:整型数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray :援用类型数组原子类
(3)援用类型
- AtomicReference:援用类型原子类
- AtomicMarkableReference:原子更新带有标记的援用类型。
- AtomicStampedReference :原子更新带有版本号的援用类型。
(4)对象的属性批改类型
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新援用类型里的字段
(5)原子累加器
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
1.1 根本类型
根本类型原子类有三个:AtomicInteger
、AtomicLong
与AtomicBoolean
。介于三个类提供的办法简直雷同,这里以整型原子类 AtomicInteger
为例来学习。
AtomicInteger 类罕用办法有:
办法 | 形容 |
---|---|
get() | 获取以后的值 |
getAndSet(int newValue) | 获取以后的值,并设置新的值 |
getAndIncrement() | 获取以后的值,并自增 |
getAndDecrement() | 获取以后的值,并自减 |
getAndAdd(int delta) | 获取以后的值,并加上预期的值 |
boolean compareAndSet(int expect, int update) | 如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update) |
lazySet(int newValue) | 最终设置为newValue,应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。 |
代码示例:
public class AtomicIntegerTest1 { public static void main(String[] args) { int temValue = 0; AtomicInteger i = new AtomicInteger(0); temValue = i.getAndSet(12); System.out.println("temValue:" + temValue + "; i:" + i); temValue = i.getAndIncrement(); System.out.println("temValue:" + temValue + "; i:" + i); temValue = i.getAndAdd(-10); System.out.println("temValue:" + temValue + "; i:" + i); }}
执行后果:
temValue:0; i:12temValue:12; i:13temValue:13; i:3
1.2 数组类型
数组类型原子类有三个:AtomicIntegerArray
、AtomicLongArray
与AtomicReferenceArray
。介于三个类提供的办法简直雷同,这里以 AtomicIntegerArray
为例来学习。
AtomicIntegerArray罕用办法
办法 | 形容 |
---|---|
get(int i) | 获取 index=i 地位元素的值 |
getAndSet(int i, int newValue) | 返回 index=i 地位的以后的值,并将其设置为新值:newValue |
getAndIncrement(int i) | 获取 index=i 地位元素的值,并让该地位的元素自增 |
getAndDecrement(int i) | 获取 index=i 地位元素的值,并让该地位的元素自减 |
getAndAdd(int i, int delta) | 获取 index=i 地位元素的值,并加上预期的值 |
compareAndSet(int i, int expect, int update) | 如果输出的数值等于预期值,则以原子形式将 index=i 地位的元素值设置为输出值(update) |
lazySet(int i, int newValue) | 最终将index=i 地位的元素设置为newValue,应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。 |
代码示例:
public class AtomicIntegerArrayTest { public static void main(String[] args) { int temValue = 0; int[] nums = {-2, -1, 1, 2, 3, 4}; AtomicIntegerArray i = new AtomicIntegerArray(nums); for (int j = 0; j < nums.length; j++) { System.out.println(i.get(j)); } temValue = i.getAndSet(0, 2); System.out.println("temValue:" + temValue + "; i:" + i); temValue = i.getAndIncrement(0); System.out.println("temValue:" + temValue + "; i:" + i); temValue = i.getAndAdd(0, 5); System.out.println("temValue:" + temValue + "; i:" + i); }}
执行后果:
-2-11234temValue:-2; i:[2, -1, 1, 2, 3, 4]temValue:2; i:[3, -1, 1, 2, 3, 4]temValue:3; i:[8, -1, 1, 2, 3, 4]Process finished with exit code 0
1.3 对象的属性批改类型
对象的属性批改类型原子类有AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
、AtomicReferenceFieldUpdater
三个。
介于三个类提供的办法简直雷同,这里以 AtomicIntegerFieldUpdater
为例来学习。
子地更新对象的属性须要两步:
- 因为对象的属性批改类型原子类都是抽象类,所以每次应用都必须应用静态方法newUpdater()创立一个更新器,并且须要设置想要更新的类和属性。
- 更新的对象属性必须应用public volatile修饰符。
AtomicIntegerFieldUpdater
类应用示例
public class AtomicIntegerFieldUpdaterTest { public class AtomicIntegerFieldUpdaterTest { public static void main(String[] args) { AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); User user = new User("ZhangSan", 20); System.out.println(a.getAndIncrement(user)); System.out.println(a.get(user)); }}class User { private String name; public volatile int age; public User(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
输入后果:
2021
1.4 援用类型
根本类型原子类只能更新一个变量,如果须要原子更新多个变量,须要应用援用类型原子类。援用类型原子类分为AtomicReference
、AtomicStampedReference
、AtomicMarkableReference
三类。
- AtomicReference:援用类型原子类
- AtomicStampedReference:原子更新带有版本号的援用类型。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记的援用类型。该类将 boolean 标记与援用关联起来。
(1)AtomicReference类
应用示例:
public class AtomicReferenceTest { public static void main(String[] args) { AtomicReference<Person> personAtomicReference = new AtomicReference<>(); Person person = new Person("Zhangsan", 20); personAtomicReference.set(person); Person updatePerson = new Person("LiSi", 30); personAtomicReference.compareAndSet(person, updatePerson); System.out.println(personAtomicReference.get().getName()); System.out.println(personAtomicReference.get().getAge()); }}class Person { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
上述代码首先创立了一个 Person 对象,而后把 Person 对象设置进 AtomicReference 对象中,而后调用 compareAndSet 办法,该办法就是通过 CAS 操作设置 personAtomicReference。如果 personAtomicReference的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 办法雷同。
输入后果:
LiSi30
(2) AtomicMarkableReference
类应用示例
AtomicMarkableReference是将一个boolean值作是否有更改的标记,实质就是它的版本号只有两个,true和false,批改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会升高ABA问题产生的几率。
public class SolveABAByAtomicMarkableReference { private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false); public static void main(String[] args) { Thread refT1 = new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); }); Thread refT2 = new Thread(() -> { boolean marked = atomicMarkableReference.isMarked(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } boolean c3 = atomicMarkableReference.compareAndSet(100, 101, marked, !marked); System.out.println(c3); // 返回true,理论应该返回false }); refT1.start(); refT2.start(); }}
输入后果:
true
1.5 原子累加器
原子类型累加器是JDK1.8引进的并发新技术,它能够看做AtomicLong和AtomicDouble的局部增强类型。
原子类型累加器的用法及原理能够参考Java多线程进阶(十七)—— J.U.C之atomic框架:LongAdder一文。
2 原子操作实现原理
在Java中能够通过锁和循环CAS的形式来实现原子操作。
2.1 简析CAS
CAS全称 Compare And Swap(比拟与替换),是一种无锁算法。在不应用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。
CAS是原子性的操作(读和写两者同时具备原子性),其实现形式是通过借助C/C++
调用CPU指令实现的,效率很高。
CAS算法波及到三个操作数:
- V 内存地址寄存的理论值
- A 比拟的旧值
- B 更新的新值
当且仅当V的值等于A时(旧值和内存中理论的值雷同),表明旧值A曾经是目前最新版本的值,自然而然能够将新值 N 赋值给 V。反之则表明V和A变量不同步,间接返回V即可。当多个线程应用CAS操作一个变量时,只有一个线程会更新胜利,其余失败的线程会从新尝试。也就是说,“更新”是一个一直重试的操作。
上面以根本原子类AtomicInteger为例,来了解原子操作的实现原理。
查看AtomicInteger源码:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // 获取并操作内存的数据。 private static final Unsafe unsafe = Unsafe.getUnsafe(); // 存储value在AtomicInteger中的偏移量。 private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // 存储AtomicInteger的int值,该属性须要借助volatile关键字保障其在线程间是可见的。 private volatile int value;
接下来,查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。
// AtomicInteger 自增办法 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } ------------------------------------------------------------------------ // Unsafe.class public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
Unsafe
还有很多个CAS
操作的相干办法,比方:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);--- omit ---
这些函数是是CAS
缩写的由来。
还是以compareAndSwapInt
为例,查看OpenJDK 8 中Unsafe.cpp的源码:
// Unsafe.javapublic final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v;}
依据OpenJDK 8的源码咱们能够看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,而后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,持续循环进行重试,直到设置胜利能力退出循环,并且将旧值返回。整个“比拟+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令实现的,属于原子操作,能够保障多个线程都可能看到同一个变量的批改值。
后续JDK通过CPU的cmpxchg指令,去比拟寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。而后通过Java代码中的while循环调用cmpxchg指令进行重试,直到设置胜利为止。
2.2 CAS实现原子操作的三大问题
CAS尽管很高效地解决了原子操作,然而CAS依然存在三大问题。ABA问题,循环工夫长开销大,以及只能保障一个共享变量的原子操作。
2.2.1 ABA问题
CAS须要在操作值的时候去查看内存中的值是否发生变化,没有发生变化才会更新内存值。然而如果一个值原来是A,变成了B,又变成了A,那么应用CAS进行查看时会发现它的值没有发生变化,然而实际上却变动了。这就是一个典型的ABA问题。
代码示例:
@Slf4j(topic = "AtomicReferenceTest")public class AtomicReferenceTest { public static void main(String[] args) { Person person1 = new Person("ZhangSan"); Person person2 = new Person("LiSi"); Person person3 = new Person("WangWu"); AtomicReference<Person> atomicReference = new AtomicReference<>(person1); log.info("success? " + atomicReference.compareAndSet(person1, person2)); log.info("success? " + atomicReference.compareAndSet(person2, person1)); log.info("success? " + atomicReference.compareAndSet(person1, person3)); }}class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; }}
输入后果:
// 三个CAS的后果都是true。阐明CAS只是比拟的两者的值是否相等,对其余内容的变动并不关怀。11:17:13.359 [main] INFO AtomicReferenceTest - success? true11:17:13.366 [main] INFO AtomicReferenceTest - success? true11:17:13.366 [main] INFO AtomicReferenceTest - success? true
ABA问题的解决思路就是在变量后面增加版本号,每次变量更新的时候都把版本号加1,这样变动过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了带版本号的援用类型AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先查看以后援用和以后标记与预期援用和预期标记是否相等,如果都相等,则以原子形式将援用值和标记的值设置为给定的更新值。
public boolean compareAndSet(V expectedReference, // 预期援用 V newReference, // 更新后援用 int expectedStamp, // 预期标记 int newStamp) { // 更新后标记 --- omit --- }
下面代码批改为:
AtomicStampedReference<Person> atomicStampedReference = new AtomicStampedReference(person1, 0); log.info("success? " + atomicStampedReference.compareAndSet(person1, person2, 0, 1)); log.info("success? " + atomicStampedReference.compareAndSet(person2, person1, 1, 2)); log.info("success? " + atomicStampedReference.compareAndSet(person1, person3, 0, 1)); log.info("success? " + atomicStampedReference.compareAndSet(person1, person3, 2, 3));
运行后果:
11:19:37.839 [main] INFO AtomicReferenceTest - success? true11:19:37.846 [main] INFO AtomicReferenceTest - success? true11:19:37.846 [main] INFO AtomicReferenceTest - success? false // 不是预期版本,CAS失败11:19:37.846 [main] INFO AtomicReferenceTest - success? true
2.2.2 循环工夫长开销大
CAS操作如果长时间不胜利,会导致其始终自旋,给CPU带来十分大的开销。
如果JVM能反对处理器提供的pause指令,那么效率会有肯定的晋升。pause指令有两个作用:第一,它能够提早流水线执行指令(de-pipeline),使CPU不会耗费过多的执行资源,提早的工夫取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它能够防止在退出循环的时候因内存程序抵触(MemoryOrder Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而进步CPU的执行效率。
2.2.3 只能保障一个共享变量的原子操作
对一个共享变量执行操作时,CAS可能保障原子操作,然而对多个共享变量操作时,CAS是无奈保障操作的原子性的。
多个共享变量操作时,个别都用锁解决。
但Java从1.5开始JDK提供了援用类型AtomicReference类来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操作。
2.3 CAS利用
(1)乐观锁
乐观锁总是假如最坏的状况,每次去操作数据时候都认为会被的线程批改数据,所以在每次操作数据的时候都会给数据加锁,让别的线程无奈操作这个数据,别的线程会始终阻塞直到获取到这个数据的锁。传统的关系型数据库里边就用到了很多这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是乐观锁思维的实现。这样的话就会影响效率,比方当有个线程产生一个很耗时的操作的时候,别的线程只是想获取这个数据的值而已都要期待很久。
(2)乐观锁
乐观锁总是假如最好的状况,每次去操作数据都认为不会被别的线程批改数据,所以在每次操作数据的时候都不会给数据加锁,即在线程对数据进行操作的时候,别的线程不会阻塞依然能够对数据进行操作,只有在须要更新数据的时候才会去判断数据是否被别的线程批改过,如果数据被批改过则会回绝操作并且返回错误信息给用户。乐观锁实用于多读的利用类型,这样能够进步吞吐量,像数据库提供的相似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包上面的原子变量类就是应用了乐观锁的一种实现形式CAS实现的。
光说概念有些形象,看下乐观锁和乐观锁的调用形式简略示例:
// ------------------------- 乐观锁的调用形式 -------------------------// synchronizedpublic synchronized void testMethod() { // 操作同步资源}// ReentrantLockprivate ReentrantLock lock = new ReentrantLock(); // 须要保障多个线程应用的是同一个锁public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock();}// ------------------------- 乐观锁的调用形式 -------------------------private AtomicInteger atomicInteger = new AtomicInteger(); // 须要保障多个线程应用的是同一个AtomicIntegeratomicInteger.incrementAndGet(); //执行自增1
参考
- 《Java并发编程的艺术》
- Java中的无锁编程
- Atomic原子类总结
- ABA问题的实质及其解决办法
- Java多线程进阶(十七)—— J.U.C之atomic框架:LongAdder