一.Atomic是什么
所谓 Atomic
,翻译过来就是原子。原子被认为是操作中最小的单位,一段代码如果是原子的,则表示这段代码在执行过程中,要么执行成功,要么执行失败。
原子操作一般都是底层通过 CPU
的指令来实现。而 atomic
包下的这些类,则可以让我们在多线程环境下,通过一种无锁的原子操作来实现线程安全。
atomic
包下的类基本上都是借助 Unsafe
类,通过 CAS
操作来封装实现的。Unsafe
这个类不属于 Java
标准,或者说这个类是 Java
预留的一个后门类,
JDK
中,有关提升性能的 concurrent
或者 NIO
等操作,大部分都是借助于这个类来封装操作的。
Java
是种编译型语言,不像 C
语言能支持操作内存,正常情况下都是由 JVM
进行内存的创建回收等操作,但这个类提供了一些直接操作内存相关的底层操作,
使得我们也可以手动操作内存,但从类的名字就可以看出,这个类不是安全的,官方也是不建议我们使用的。
二.Atomic能做什么
在多线程或者并发环境中,常常会遇到这种情况 int i=0; i++ 。稍有经验的同学都知道这种写法是线程不安全的。
为了达到线程安全的目的,通常会用synchronized来修饰对应的代码块。现在我们有了新的方法,就是使用J.U.C包下的atomic类。
三.Atomic原理
一句话来说,atomic类是通过自旋CAS操作volatile变量实现的。
CAS是compare and swap的缩写,即比较后(比较内存中的旧值与预期值)交换(将旧值替换成预期值)。它是sun.misc包下Unsafe类提供的功能,需要底层硬件指令集的支撑。
使用volatile变量是为了多个线程间变量的值能及时同步。
按理来说,使用synchroized已经能满足功能需求了。为什么还会有这个类呢?那肯定是性能的问题了。
在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。
而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一中实现方式。这种方式就避免了直接使用内核状态的重量级锁。
但是在JDK1.6以后,synchronized进行了优化,引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。
四.Atomic使用
Atomic包里一共有12个类,主要提供四种原子更新方式:
1、原子方式更新基本类型
以下三个类是以原子方式更新基本类型
(1)AtomicBoolean:原子更新布尔类型。
(2)AtomicInteger:原子更新整型。
(3)AtomicLong:原子更新长整型。
以AtomicInteger为例:
package concurrency; import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { static AtomicInteger ai = new AtomicInteger(1); public static void main(String[] args) { //相当于i++,返回的是旧值,看方法名就知道,先获取再自增 System.out.println(ai.getAndIncrement()); System.out.println(ai.get()); //先自增,再获取 System.out.println(ai.incrementAndGet()); System.out.println(ai.get()); //增加一个指定值,先add,再get System.out.println(ai.addAndGet(5)); System.out.println(ai.get()); //增加一个指定值,先get,再set System.out.println(ai.getAndSet(5)); System.out.println(ai.get()); } }
注意:
Atomic包提供了三种基本类型的原子更新,剩余的Java的基本类型还有char,float和double等,其更新方式可以参考AtomicBoolean的思路来现,
AtomicBoolean是把boolean转成整型再调用compareAndSwapInt进行CAS来实现的,类似的short和byte也可以转成整形,float和double可以利用Float.floatToIntBits,
Double.doubleToLongBits转成整形和长整形进行相应处理;
2、原子方式更新数组
以下三个类是以原子方式更新数组,
(1)AtomicIntegerArray:原子更新整型数组里的元素。
(2)AtomicLongArray:原子更新长整型数组里的元素。
(3)AtomicReferenceArray:原子更新引用类型数组里的元素。
以AtomicIntegerArray为例,其方法与AtomicInteger很像,多了个数组下标索引;
package concurrency; import java.util.concurrent.atomic.AtomicIntegerArray; public class AtomicIntegerArrayTest { static int[] valueArr = new int[] { 1, 2 }; //AtomicIntegerArray内部会拷贝一份数组 static AtomicIntegerArray ai = new AtomicIntegerArray(valueArr); public static void main(String[] args) { ai.getAndSet(0, 3); //不会修改原始数组value System.out.println(ai.get(0)); System.out.println(valueArr[0]); } }
3、原子方式更新引用
以下三个类是以原子方式更新引用,与其它不同的是,更新引用可以更新多个变量,而不是一个变量;
(1)AtomicReference:原子更新引用类型。
(2)AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
(3)AtomicMarkableReference:原子更新带有标记位的引用类型。
以AtomicReference为例:
package concurrency; import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { public static AtomicReference<User> atomicUserRef = new AtomicReference<User>(); public static void main(String[] args) { User user = new User("conan", 15); atomicUserRef.set(user); User updateUser = new User("Shinichi", 17); atomicUserRef.compareAndSet(user, updateUser); System.out.println(atomicUserRef.get().getName()); System.out.println(atomicUserRef.get().getOld()); } static class User { private String name; private int old; public User(String name, int old) { this.name = name; this.old = old; } public String getName() { return name; } public int getOld() { return old; } } }
4、原子方式更新字段
以下三个类是以原子方式更新字段:
(1)AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
(2)AtomicLongFieldUpdater:原子更新长整型字段的更新器。
(3)AtomicStampedReference:原子更新带有版本号的引用类型,用于解决使用CAS进行原子更新时,可能出现的ABA问题。
以AtomicIntegerFieldUpdater为例:
package concurrency; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class AtomicIntegerFieldUpdaterTest { private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater .newUpdater(User.class, "old"); public static void main(String[] args) { User conan = new User("conan", 10); System.out.println(a.getAndIncrement(conan)); System.out.println(a.get(conan)); } public static class User { private String name; //注意需要用volatile修饰 public volatile int old; public User(String name, int old) { this.name = name; this.old = old; } public String getName() { return name; } public int getOld() { return old; } } }
5、Atomic类的缺点
(1)ABA问题:
对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。
解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。
atomic包下AtomicStampedReference类实现了这种思路。Mysql中Innodb的多版本并发锁也是这个原理。
(2)自旋问题:
atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,
但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。
因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。
当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。
6、总结
对于jdk1.8的并发包来说,底层基本上就是通过Usafe和CAS机制来实现的。有好处也肯定有一个坏处。
从好的方面来讲,就是上面AtomicInteger类可以保持其原子性。
但是从坏的方面来看,Usafe因为直接操作的底层地址,肯定不是那么安全,而且CAS机制也伴随着大量的问题,比如说有名的ABA问题等等。
JDK8
又引入了下面几个类:DoubleAdder
,LongAdder
,DoubleAccumulator
,LongAccumulator
,这些类是对AtomicLong
这些类的改进与增强,这些类都继承自Striped64
这个类。