atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,所以,所谓原子类说简单点就是具有原子操作特征的类,原子操作类提供了一些修改数据的方法,这些方法都是原子操作的,在多线程情况下可以确保被修改数据的正确性,我们在前边的Java并发机制底层实现中了解到,通过CAS操作可以实现原子操作,整体分类如下
基本原子类
基本原子类型包含三种,都比较简单,这里我们以AtomicInteger为例进行介绍:
AtomicInteger:int 类型原子类 AtomicLong:long 类型原子类 AtomicBoolean :boolean类型原子类
AtomicInteger的常用方法如下:
public final int get() //获取当前的值 public final int getAndSet(int newValue)//获取当前的值,并设置新的值 public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) boolean compareAndSet(int expect, int update) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 public final void lazySet(int newValue)
我们从其实现的源码中可以看出:
private static final Unsafe unsafe = Unsafe.getUnsafe(); //value属性在AtomicInteger中的偏移量,通过这个偏移量可以快速定位到value字段,这个是实现AtomicInteger的关键 private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //使用volatile修饰,可以确保value在多线程中的可见性。
可以通过一个方法的源码来看其调用方式:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 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)); //CAS自旋等待,多线程情况下安全 return var5; } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
getAndAddInt操作相当于线程安全的count++操作
synchronize(lock){ count++; }
synchronize的方式会导致占时无法获取锁的线程处于阻塞状态,性能比较低。CAS的性能比synchronize要快很多
数组原子类
使用原子的方式更新数组里的某个元素,可以确保修改数组中数据的线程安全性
AtomicIntegerArray:整形数组原子操作类 AtomicLongArray:长整形数组原子操作类 AtomicReferenceArray :引用类型数组原子操作类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍
public final int get(int i) //获取 index=i 位置元素的值 //返回 index=i 位置的当前的值,并将其设置为新值:newValue public final int getAndSet(int i, int newValue) public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值 //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) boolean compareAndSet(int expect, int update) //最终 将index=i 位置的元素设置为newValue, //使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 public final void lazySet(int i, int newValue)
示例如下:
public class ThreadTest { static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]); public static void request(int page) throws InterruptedException { //模拟耗时5毫秒 TimeUnit.MILLISECONDS.sleep(5); //pageCountIndex为pageCount数组的下标,表示页面对应数组中的位置 int pageCountIndex = page - 1; pageRequest.incrementAndGet(pageCountIndex); } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { try { for (int page = 1; page <= 10; page++) { for (int j = 0; j < 10; j++) { request(page); } } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime)); for (int pageIndex = 0; pageIndex < 10; pageIndex++) { System.out.println("索引值为" + (pageIndex + 1) + " 被累加次数为" + pageRequest.get(pageIndex)); } } }
执行结果为:
main,耗时:650 索引值为1 被累加次数为1000 索引值为2 被累加次数为1000 索引值为3 被累加次数为1000 索引值为4 被累加次数为1000 索引值为5 被累加次数为1000 索引值为6 被累加次数为1000 索引值为7 被累加次数为1000 索引值为8 被累加次数为1000 索引值为9 被累加次数为1000 索引值为10 被累加次数为1000
其实其底层也是调用了普通原子类实现
引用原子类
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类
AtomicReference:引用类型原子类 AtomicStampedRerence:原子更新引用类型里的字段原子类 //可以用时间戳解决ABA问题 AtomicMarkableReference :原子更新带有标记位的引用类型
ABA问题
普通情况下ABA问题没有危害,不过可以看一种特殊场景,场景是用链表来实现一个栈,初始化向栈中压入B、A两个元素,栈顶head指向A元素。head(A)->B
Thread thread1 = new Thread( ->{ oldValue = head; sleep(3秒); //thread2切换执行 compareAndSet(oldValue, B); } ); Thread thread2 = new Thread( ->{ // 弹出A newHead = head.next; head.next = null; //即A.next = null; head = newHead; // 弹出B newHead = head.next; head.next = null; // 即B.next = null; head = newHead; // 此时head为null // 压入C head = C; // 压入D D.next = head; head = D; // 压入A A.next = D; head = A; } ); thread1.start(); thread2.start();
- 线程1试图将栈顶换成B,但它获取栈顶的oldValue(head,也就是A)后,被线程2中断了。
- 线程2依次将A、B弹出,然后压入C、D、A。
head(A)->D->C
- 然后换线程1继续运行,线程1执行compareAndSet发现head指向的元素确实与oldValue一致,都是A,所以就将head指向B了。
head(B)
但是,线程2在弹出B的时候,将B的next置为null了,因此在线程1将head指向B后,栈中只剩元素B。但按预期来说,栈中应该放的是B → A → D → C
AtomicStampedRerence
可以解决ABA问题,他内部不仅维护了对象的值,还维护了一个时间戳(我们这里把他称为时间戳,实际上它可以使用任何一个整形来表示状态值),当AtomicStampedRerence
对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedRerence
设置对象值时,对象值及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变量,就能防止不恰当的写入
更新原子类
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改原子类
AtomicIntegerFieldUpdater:原子更新整形字段的值 AtomicLongFieldUpdater:原子更新长整形字段的值 AtomicReferenceFieldUpdater :原子更新引用类型字段的值
要想原子地更新对象的属性需要两步:
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
- 更新的对象属性必须使用 public volatile 修饰符。
上面三个类提供的方法几乎相同,所以我们这里以AtomicReferenceFieldUpdater为例子来介绍。
调用AtomicReferenceFieldUpdater静态方法newUpdater创建AtomicReferenceFieldUpdater对象
//tclass:需要操作的字段所在的类,vclass:操作字段的类型,fieldName:字段名称 public static <U, W> AtomicReferenceFieldUpdater<U, W> newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)
多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
public class Demo5 { static Demo5 demo5 = new Demo5(); //isInit用来标注是否被初始化过 volatile Boolean isInit = Boolean.FALSE; AtomicReferenceFieldUpdater<Demo5, Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(Demo5.class, Boolean.class, "isInit"); public void init() throws InterruptedException { //isInit为false的时候,才进行初始化,并将isInit采用原子操作置为true if (updater.compareAndSet(demo5, Boolean.FALSE, Boolean.TRUE)) { System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",开始初始化!"); //模拟休眠3秒 TimeUnit.SECONDS.sleep(3); System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完毕!"); } else { System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其他线程已经执行了初始化!"); } } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { try { demo5.init(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } }
返回结果为:
1565159962098,Thread-0,开始初始化! 1565159962098,Thread-3,有其他线程已经执行了初始化! 1565159962098,Thread-4,有其他线程已经执行了初始化! 1565159962098,Thread-2,有其他线程已经执行了初始化! 1565159962098,Thread-1,有其他线程已经执行了初始化! 1565159965100,Thread-0,初始化完毕!
可以看出多线程同时执行init()方法,只有一个线程执行了初始化的操作,其他线程跳过了。多个线程同时到达updater.compareAndSet,只有一个会成功
增强原子类
原子类的基本实现机制,它们都是在一个死循环内,不断尝试修改目标值,知道修改成功。如果竞争不激烈,那么修改成功的概率就很高,否则,修改失败的概率就很高。在大量修改失败时,这些原子操作就会进行多次循环尝试,因此性能会受到影响。
当竞争激烈的时候,为了进一步提高系统的性能,一种基本方案就是可以使用热点分离,将竞争的数据进行分解,基于这个思路,可以想到一种对传统AtomicInteger等原子类的改进方法。虽然在CAS操作中没有锁,但是像减小锁粒度这种分离热点的思想依然可以使用。一种可行的方案就是仿造ConcurrentHashMap,将热点数据分离。比如,可以将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计算,而最终的计算结果,则为这个数组的求和累加。热点value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度。增强原子类正是利用了这种原理实现,这里不详细介绍了。