注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是第2行创建的tl对象)。接着提交了第2次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第2次GC。可以看到,在第2次GC后,第一次创建的10个SimpleDateFormat子类实例全部被回收。可以看到,虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们(注意,这段代码是在JDK 7中输出的,在JDK 8中,你也许得不到类似的输出,大家可以比较两个JDK版本之间线程持有ThreadLocal变量的不同)。
要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西。更精确地说,它更加类似WeakHashMap。
ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>:
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
这里的参数k就是Map的key,v就是Map的value。其中k也就是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Map的key,但是实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在我们这个案例中,它奏效了),就会自然将这些垃圾数据回收。整个结构如图4.1所示。
图4.1 ThreadLocal的回收机制
4.3.3 对性能有何帮助
为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。
这里,让我们简单测试一下在多线程下产生随机数的性能问题。首先,我们定义一些全局变量:
01 public static final int GEN_COUNT = 10000000; 02 public static final int THREAD_COUNT = 4; 03 static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT); 04 public static Random rnd = new Random(123); 05 06 public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() { 07 @Override 08 protected Random initialValue() { 09 return new Random(123); 10 } 11 };
代码第1行定义了每个线程要产生的随机数数量,第2行定义了参与工作的线程数量,第3行定义了线程池,第4行定义了被多线程共享的Random实例用于产生随机数,第6~11行定义了由ThreadLocal封装的Random。
接着,定义一个工作线程的内部逻辑。它可以工作在两种模式下:
第一是多线程共享一个Random(mode=0),
第二是多个线程各分配一个Random(mode=1)。
01 public static class RndTask implements Callable<Long> { 02 private int mode = 0; 03 04 public RndTask(int mode) { 05 this.mode = mode; 06 } 07 08 public Random getRandom() { 09 if (mode == 0) { 10 return rnd; 11 } else if (mode == 1) { 12 return tRnd.get(); 13 } else { 14 return null; 15 } 16 } 17 18 @Override 19 public Long call() { 20 long b = System.currentTimeMillis(); 21 for (long i = 0; i < GEN_COUNT; i++) { 22 getRandom().nextInt(); 23 } 24 long e = System.currentTimeMillis(); 25 System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms"); 26 return e - b; 27 } 28 }
上述代码第19~27行定义了线程的工作内容。每个线程会产生若干个随机数,完成工作后,记录并返回所消耗的时间。
最后是我们的main()函数,它分别对上述两种情况进行测试,并打印了测试的耗时:
01 public static void main(String[] args) throws InterruptedException, ExecutionException { 02 Future<Long>[] futs = new Future[THREAD_COUNT]; 03 for (int i = 0; i < THREAD_COUNT; i++) { 04 futs[i] = exe.submit(new RndTask(0)); 05 } 06 long totaltime = 0; 07 for (int i = 0; i < THREAD_COUNT; i++) { 08 totaltime += futs[i].get(); 09 } 10 System.out.println("多线程访问同一个Random实例:" + totaltime + "ms"); 11 12 //ThreadLocal的情况 13 for (int i = 0; i < THREAD_COUNT; i++) { 14 futs[i] = exe.submit(new RndTask(1)); 15 } 16 totaltime = 0; 17 for (int i = 0; i < THREAD_COUNT; i++) { 18 totaltime += futs[i].get(); 19 } 20 System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms"); 21 exe.shutdown(); 22 }
上述代码的运行结果,可能如下:
pool-1-thread-3 spend 3398ms pool-1-thread-1 spend 3436ms pool-1-thread-2 spend 3495ms pool-1-thread-4 spend 3513ms 多线程访问同一个Random实例:13842ms pool-1-thread-4 spend 375ms pool-1-thread-1 spend 429ms pool-1-thread-2 spend 453ms pool-1-thread-3 spend 499ms 使用ThreadLocal包装Random实例:1756ms
很明显,在多线程共享一个Random实例的情况下,总耗时达13秒之多(这里是指4个线程的耗时总和,不是程序执行的经历时间)。而在ThreadLocal模式下,仅耗时1.7秒左右。
4.4 无锁
就人的性格而言,我们可以分为乐天派和悲观派。对于乐天派来说,总是会把事情往好的方面想。他们认为所有事情总是不太容易发生问题,出错是小概率的,所以我们可以肆无忌惮地做事。如果真的不幸遇到了问题,则有则改之无则加勉。而对于悲观的人群来说,他们总是担惊受怕,认为出错是一种常态,所以无论巨细,都考虑得面面俱到,滴水不漏,确保为人处世,万无一失。
对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
4.4.1 与众不同的并发策略:比较交换(CAS)
与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
CAS算法的过程是这样:它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
4.4.2 无锁的线程安全整数:AtomicInteger
为了让Java程序员能够受益于CAS等CPU指令,JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。
其中,最常用的一个类,应该就是AtomicInteger。你可以把它看做是一个整数。但是与Integer不同,它是可变的,并且是线程安全的。对其进行修改等任何操作,都是用CAS指令进行的。这里简单列举一下AtomicInteger的一些主要方法,对于其他原子类,操作也是非常类似的:
public final int get() //取得当前值 public final void set(int newValue) //设置当前值 public final int getAndSet(int newValue) //设置新值,并返回旧值 public final boolean compareAndSet(int expect, int u) //如果当前值为expect,则设置为u public final int getAndIncrement() //当前值加1,返回旧值 public final int getAndDecrement() //当前值减1,返回旧值 public final int getAndAdd(int delta) //当前值增加delta,返回旧值 public final int incrementAndGet() //当前值加1,返回新值 public final int decrementAndGet() //当前值减1,返回新值 public final int addAndGet(int delta) //当前值增加delta,返回新值
就内部实现上来说,AtomicInteger中保存一个核心字段:
private volatile int value;
它就代表了AtomicInteger的当前实际取值。此外还有一个:
private static final long valueOffset;
它保存着value字段在AtomicInteger对象中的偏移量。后面你会看到,这个偏移量是实现AtomicInteger的关键。
AtomicInteger的使用非常简单,这里给出一个示例:
01 public class AtomicIntegerDemo { 02 static AtomicInteger i=new AtomicInteger(); 03 public static class AddThread implements Runnable{ 04 public void run(){ 05 for(int k=0;k<10000;k++) 06 i.incrementAndGet(); 07 } 08 } 09 public static void main(String[] args) throws InterruptedException { 10 Thread[] ts=new Thread[10]; 11 for(int k=0;k<10;k++){ 12 ts[k]=new Thread(new AddThread()); 13 } 14 for(int k=0;k<10;k++){ts[k].start();} 15 for(int k=0;k<10;k++){ts[k].join();} 16 System.out.println(i); 17 } 18 }
第6行的AtomicInteger.incrementAndGet()方法会使用CAS操作将自己加1,同时也会返回当前值(这里忽略了当前值)。如果你执行这段代码,你会看到程序输出了100000。这说明程序正常执行,没有错误。如果不是线程安全,i的值应该会小于100000才对。
使用AtomicInteger会比使用锁具有更好的性能。出于篇幅限制,这里不再给出AtomicInteger和锁的性能对比的测试代码,相信写一段简单的小代码测试两者的性能应该不是难事。这里让我们关注一下incrementAndGet()的内部实现(我们基于JDK 1.7分析,JDK 1.8与1.7的实现有所不同)。
1 public final int incrementAndGet() { 2 for (;;) { 3 int current = get(); 4 int next = current + 1; 5 if (compareAndSet(current, next)) 6 return next; 7 } 8 }
其中get()方法非常简单,就是返回内部数据value。
public final int get() { return value; }
这里让人映像深刻的,应该是incrementAndGet()方法的第2行for循环吧!如果你是初次看到这样的代码,可能会觉得很奇怪,为什么连设置一个值那么简单的操作都需要一个死循环呢?原因就是:CAS操作未必是成功的,因此对于不成功的情况,我们就需要进行不断的尝试。第3行的get()取得当前值,接着加1后得到新值next。这里,我们就得到了CAS必需的两个参数:期望值以及新值。使用compareAndSet()方法将新值next写入,成功的条件是在写入的时刻,当前的值应该要等于刚刚取得的current。如果不是这样,就说明AtomicInteger的值在第3行到第5行代码之间,又被其他线程修改过了。当前线程看到的状态就是一个过期状态。因此,compareAndSet返回失败,需要进行下一次重试,直到成功。
以上就是CAS操作的基本思想。在后面我们会看到,无论程序多么复杂,其基本原理总是不变的。
和AtomicInteger类似的类还有AtomicLong用来代表long型,AtomicBoolean表示boolean型,AtomicReference表示对象引用。
4.4.3 Java中的指针:Unsafe类
如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet()方法中compareAndSet()的实现。现在,就让我们更进一步看一下它吧!
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
在这里,我们看到一个特殊的变量unsafe,它是sun.misc.Unsafe类型。从名字看,这个类应该是封装了一些不安全的操作。那什么操作是不安全的呢?学习过C或者C++的话,大家应该知道,指针是不安全的,这也是在Java中把指针去除的重要原因。如果指针指错了位置,或者计算指针偏移量时出错,结果可能是灾难性的,你很有可能会覆盖别人的内存,导致系统崩溃。
而这里的Unsafe就是封装了一些类似指针的操作。compareAndSwapInt()方法是一个navtive方法,它的几个参数含义如下:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值。如果指定的字段的值等于expected,那么就会把它设置为x。
不难看出,compareAndSwapInt()方法的内部,必然是使用CAS原子指令来完成的。此外,Unsafe类还提供了一些方法,主要有以下几个(以Int操作为例,其他数据类型是类似的):
//获得给定对象偏移量上的int值 public native int getInt(Object o, long offset); //设置给定对象偏移量上的int值 public native void putInt(Object o, long offset, int x); //获得字段在对象中的偏移量 public native long objectFieldOffset(Field f); //设置给定对象的int值,使用volatile语义 public native void putIntVolatile(Object o, long offset, int x); //获得给定对象对象的int值,使用volatile语义 public native int getIntVolatile(Object o, long offset); //和putIntVolatile()一样,但是它要求被操作字段就是volatile类型的 public native void putOrderedInt(Object o, long offset, int x);
如果大家还记得“3.3.4深度剖析ConcurrentLinkedQueue”一节中描述的ConcurrentLinkedQueue实现,应该对ConcurrentLinkedQueue中的Node还有些印象。Node的一些CAS操作也都是使用Unsafe类来实现的。大家可以回顾一下,以加深对Unsafe类的印象。
这里就可以看到,虽然Java抛弃了指针。但是在关键时刻,类似指针的技术还是必不可少的。这里底层的Unsafe实现就是最好的例子。但是很不幸,JDK的开发人员并不希望大家使用这个类。获得Unsafe实例的方法是调动其工厂方法getUnsafe()。但是,它的实现却是这样:
public static Unsafe getUnsafe() { Class cc = Reflection.getCallerClass(); if (cc.getClassLoader() != null) throw new SecurityException("Unsafe"); return theUnsafe; }
注意加粗部分的代码,它会检查调用getUnsafe()函数的类,如果这个类的ClassLoader不为null,就直接抛出异常,拒绝工作。因此,这也使得我们自己的应用程序无法直接使用Unsafe类。它是一个JDK内部使用的专属类。
注意:根据Java类加载器的工作原理,应用程序的类由App Loader加载。而系统核心类,如rt.jar中的类由Bootstrap类加载器加载。Bootstrap加载器没有Java对象的对象,因此试图获得这个类加载器会返回null。所以,当一个类的类加载器为null时,说明它是由Bootstrap加载的,而这个类也极有可能是rt.jar中的类。
4.4.4 无锁的对象引用:AtomicReference
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我希望同时提出一个有关原子操作的逻辑上的不足。
之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过这两次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。如图4.2所示,显示了这种情况。
图4.2 对象值被反复修改回原数据
一般来说,发生这种情况的概率很小。而且即使发生了,可能也不是什么大问题。比如,我们只是简单地要做一个数值加法,即使在我取得期望值后,这个数字被不断的修改,只要它最终改回了我的期望值,我的加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,所有的信息都只保存于对象的数值本身。
但是,在现实中,还可能存在另外一种场景,就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。
打一个比方,如果有一家蛋糕店,为了挽留客户,决定为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。
现在,我们就来模拟这个场景,为了演示AtomicReference,我在这里使用AtomicReference实现这个功能。首先,我们模拟用户账户余额。
定义用户账户余额:
static AtomicReference<Integer> money=new AtomicReference<Integer>(); // 设置账户初始值小于20,显然这是一个需要被充值的账户 money.set(19);
接着,我们需要若干个后台线程,它们不断扫描数据,并为满足条件的客户充值。
01 //模拟多个线程同时更新后台数据库,为用户充值 02 for(int i = 0 ; i < 3 ; i++) { 03 new Thread() { 04 public void run() { 05 while(true){ 06 while(true){ 07 Integer m=money.get(); 08 if(m<20){ 09 if(money.compareAndSet(m, m+20)){ 10 System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元"); 11 break; 12 } 13 }else{ 14 //System.out.println("余额大于20元,无须充值"); 15 break ; 16 } 17 } 18 } 19 } 20 }.start(); 21 }
上述代码第8行,判断用户余额并给予赠送金额。如果已经被其他用户处理,那么当前线程就会失败。因此,可以确保用户只会被充值一次。
此时,如果很不幸,用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于20元,并且正好累计消费了20元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能。下面模拟了这个消费线程:
01 //用户消费线程,模拟消费行为 02 new Thread() { 03 public void run() { 04 for(int i=0;i<100;i++){ 05 while(true){ 06 Integer m=money.get(); 07 if(m>10){ 08 System.out.println("大于10元"); 09 if(money.compareAndSet(m, m-10)){ 10 System.out.println("成功消费10元,余额:"+money.get()); 11 break; 12 } 13 }else{ 14 System.out.println("没有足够的金额"); 15 break; 16 } 17 } 18 try {Thread.sleep(100);} catch (InterruptedException e) {} 19 } 20 } 21 }.start();
上述代码中,消费者只要贵宾卡里的钱大于10元,就会立即进行一次10元的消费。执行上述程序,得到的输出如下:
余额小于20元,充值成功,余额:39元 大于10元 成功消费10元,余额:29 大于10元 成功消费10元,余额:19 余额小于20元,充值成功,余额:39元 大于10元 成功消费10元,余额:29 大于10元 成功消费10元,余额:39 余额小于20元,充值成功,余额:39元
从这一段输出中,可以看到,这个账户被先后反复多次充值。其原因正是因为账户余额被反复修改,修改后的值等于原有的数值,使得CAS操作无法正确判断当前数据状态。
虽然说这种情况出现的概率不大,但是依然是有可能出现的。因此,当业务上确实可能出现这种情况时,我们也必须多加防范。体贴的JDK也已经为我们考虑到了这种情况,使用AtomicStampedReference就可以很好地解决这个问题。
4.4.5 带有时间戳的对象引用:AtomicStampedReference
AtomicReference无法解决上述问题的根本因为是对象在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好地解决对象被反复修改导致线程无法正确判断对象状态的问题。
AtomicStampedReference正是这么做的。它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
AtomicStampedReference的几个API在AtomicReference的基础上新增了有关时间戳的信息:
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳 public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) //获得当前对象引用 public V getReference() //获得当前时间戳 public int getStamp() //设置当前对象引用和时间戳 public void set(V newReference, int newStamp)
有了AtomicStampedReference这个法宝,我们就再也不用担心对象被写坏啦!现在,就让我们使用AtomicStampedReference来修正那个贵宾卡充值的问题:
01 public class AtomicStampedReferenceDemo { 02 static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0); 03 public static void main(String[] args) { 04 //模拟多个线程同时更新后台数据库,为用户充值 05 for(int i = 0 ; i < 3 ; i++) { 06 final int timestamp=money.getStamp(); 07 new Thread() { 08 public void run() { 09 while(true){ 10 while(true){ 11 Integer m=money.getReference(); 12 if(m<20){ 13 if(money.compareAndSet(m, m+20,timestamp,timestamp+1)){ 14 System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元"); 15 break; 16 } 17 }else{ 18 //System.out.println("余额大于20元,无须充值"); 19 break ; 20 } 21 } 22 } 23 } 24 }.start(); 25 } 26 27 //用户消费线程,模拟消费行为 28 new Thread() { 29 public void run() { 30 for(int i=0;i<100;i++){ 31 while(true){ 32 int timestamp=money.getStamp(); 33 Integer m=money.getReference(); 34 if(m>10){ 35 System.out.println("大于10元"); 36 if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){ 37 System.out.println("成功消费10元,余额:"+money.getReference()); 38 break; 39 } 40 }else{ 41 System.out.println("没有足够的金额"); 42 break; 43 } 44 } 45 try {Thread.sleep(100);} catch (InterruptedException e) {} 46 } 47 } 48 }.start(); 49 } 50 }
第2行,我们使用AtomicStampedReference代替原来的AtomicReference。第6行获得账户的时间戳,后续的赠予操作以这个时间戳为依据。如果赠予成功(第13行),则修改时间戳,使得系统不可能发生二次赠予的情况。消费线程也是类似,每次操作,都使得时间戳加1(第36行),使之不可能重复。
执行上述代码,可以得到以下输出:
余额小于20元,充值成功,余额:39元 大于10元 成功消费10元,余额:29 大于10元 成功消费10元,余额:19 大于10元 成功消费10元,余额:9 没有足够的金额
可以看到,账户只被赠予了一次。
4.4.6 数组也能无锁:AtomicIntegerArray
除了提供基本数据类型外,JDK还为我们准备了数组等复合结构。当前可用的原子数组有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别表示整数数组、long型数组和普通的对象数组。
这里以AtomicIntegerArray为例,展示原子数组的使用方式。
AtomicIntegerArray本质上是对int[]类型的封装,使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。它提供了以下几个核心API:
//获得数组第i个下标的元素 public final int get(int i) //获得数组的长度 public final int length() //将数组第i个下标设置为newValue,并返回旧的值 public final int getAndSet(int i, int newValue) //进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true public final boolean compareAndSet(int i, int expect, int update) //将第i个下标的元素加1 public final int getAndIncrement(int i) //将第i个下标的元素减1 public final int getAndDecrement(int i) //将第i个下标的元素增加delta(delta可以是负数) public final int getAndAdd(int i, int delta)
下面给出一个简单的示例,展示AtomicIntegerArray的使用: