学习资料:《Java高并发程序设计》以及极客时间的Java并发编程实战课程
后续会根据《深入理解计算机系统》,《Java并发编程的艺术》,《Java并发编程实战》 等内容补充完善
以下大部分阐述来自上述书籍与课程中个人认为很重要的部分,也有部分心得体会,后续还会更新各种并发相关笔记,点点关注不迷路!ヽ(✿゚▽゚)ノ
一.JAVA中多线程同步控制的工具
有了synchronized,为什么还要再造管程?
synchronized 没有办法破坏不可抢占条件,即在申请资源的时候,如果申请不到,线程直接进入阻塞状态了,无法释放不了线程已经占有的资源,会导致死锁。
那么什么样的锁能解决这个问题呢?
1.能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
2.支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
3.非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
三个方案对应的方法
// 支持中断的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
如何保证可见性?
利用了 volatile 相关的 Happens-Before 规则
内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值
1.重入锁
重入锁是synchronized的强化版。
包:java.util.concurrent.locks.ReentrantLock
它名字的由来是因为在一个线程中,可以反复加锁或者释放锁多次。如果释放锁的次数比加锁次数多会得到java.lang.IllegalMonitorStateExecption的异常。
1)中断响应 lockInterruptibly()
不同于synchronized,ReentrantLock可以在一个线程申请锁的过程中中断它,来处理死锁问题。
public static ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly();//记得捕获InterruptedException
lockInterruptibly() 这是一个可以对中断进行相应的锁申请动作,即在等待锁的过程中,可以响应中断。
补充
锁一般是不响应中断异常,所以重入锁响应中断异常的时候,要加Interruptibly;相对的,await()方法一般是响应中断异常,所以不响应中断异常的时候,要加Uninterruptibly
2)锁申请等待显示 tryLock()
tryLock()方法可以接受两个参数,第一个代表时间长度,第二个代表时间的单位(秒/分/时…)。成功申请锁返回true,申请失败返回false。
public boolean tryLock();
public boolean tryLock(long timeout, TimeUnit unit);
3)公平锁
他会按照时间的先后顺序,保证先到者先得。
public ReentrantLock(boolean fair)
当fair为true的时候,表示锁是公平的。但它的实现成本比较高,性能非常低下,所以若非特别需要,不推荐使用。
补充(后面具体讲)
重入锁实现的三个要素
1)原子状态:用CAS操作来存储当前锁的转台。
2)等待队列:所有没有请求到锁的线程,会进入等待队列。
3)阻塞原语park()和unpark(),用来挂起和恢复线程。
重入锁拥有synchronized许多不具有的功能,但这并不是代表我们能用它完全取代synchronized。
官方表示,他们更支持synchronize,它在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
synchronized相较于ReentrantLock的优点:
1.减少内存开销:
如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
2.内部优化:
synchronized是JVM直接支持的。
JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。(jdk1.6以后添加的)
2.Condition
包:java.util.concurrent.locks.Condition
作用:Condition的作用与wait()和notify()相似
方法:
void await() throws InterruptedException;
void awaitUninterruptibly();
boolean await(long time, TimeUnit unit) throws InterruptedException;
void signal();
void signalAll();
await()会让当前线程等待,释放锁,当其他线程使用signal()或signalAll()时,会重新回到Runnable状态(同wait()和notify())
awaitUninterruptibly同await相似,只是不相应中断。
补充:signal()以后一定要unlock()释放锁,因为原线程必须要获得锁才能继续运行。
而synchronized不用显示释放锁的原因是它会自动释放锁。
3.信号量 Semaphore
包:java.util.concurrent.Semaphore
简介:信号量是对锁的扩展,可以指定多个线程,同时访问同一个资源。信号量模型可以简单概括为:一个计数器,一个等待队列,三个方法init()、down() 和 up()。
init():计数器的初始值。对应java里的构造函数中的permits。
down():计数器的值减 1;如果此时计数器的值小于
0,则当前线程将被阻塞,否则当前线程可以继续执行。对应java中的acquire();
up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。对应java中的release();
构造函数:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
permits指的是准入数,也就是访问同一个资源的线程的阈值。
方法:
public void acquire() throws InterruptedException
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
acquire()类似于await(),区别是申请获得的是许可(锁只有一个,许可有permits个),是原子操作,使计数器-1。
tryAcquire()与之前的tryLock()也很相似,就不赘述了。
release就是释放许可,与之前的unlock()类似,是原子操作,使计数器+1。
信号量 leetcode练习题:链接
4.读写锁 ReadWriteLock
包:java.util.concurrent.locks.ReentrantReadWriteLock
作用:ReadWriteLock是JDK5中的读写分离锁,允许多个线程并行读。
适用场景:在读操作远远大于写操作的时候,用读写锁能大幅提高效率。
构造方法:
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readlock = readWriteLock.readLock();
private static Lock writelock = readWriteLock.writeLock();
只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。
补充
更快的读写锁——StampedLock
StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。
StampedLock 不支持重入,StampedLock 的悲观读锁、写锁都不支持条件变量。使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。
StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。乐观读tryOptimisticRead()这个操作是无锁的。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。一般做法是循环乐观读,如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。
StampedLock 还支持锁的降级(通过 tryConvertToReadLock() 方法实现)和升级(通过 tryConvertToWriteLock() 方法实现),不过使用的时候应谨慎。
StampedLock 读模板
final StampedLock sl =
new StampedLock();
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 读入方法局部变量
.....
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
// 使用方法局部变量执行业务操作
......
StampedLock 写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
5.倒计数器 CountDownLatch
包:java.util.concurrent.CountDownLatch
作用:等待指定数量的线程完成后开始执行
构造函数:接受一个整数作为参数,即这个计数器的计数个数
public CountDownLatch(int count)
方法:
public void countDown()
public void await() throws InterruptedException
countDown()通知计数器,一个线程已经完成。
await()方法要求主线程等待所有任务完成再继续。
6.循环栅栏 CyclicBarrier
包:java.util.concurrent.CyclicBarrier
作用:与之前的CountDownLatch类似,不同的是,计数器满的时候会重新归零。
构造方法:parties为计数总数,barrierAction为计数器满后会进行的动作。
public CyclicBarrier(int parties, Runnable barrierAction)
方法:
public int await() throws InterruptedException, BrokenBarrierException
等待计数器满后,才会执行主线程。
线程中断时,会InterruptedException。
CyclicBarrier破损时,会BrokenBarrierException,可能系统已经没有办法等待所有线程到齐了,可能是其中某个线程被中断了。
7.线程阻塞工具类 LockSupport
包:java.util.concurrent.locks.LockSupport
作用:它可以在线程内任何位置让线程阻塞,不需要获得某个对象的锁,也不会抛出InterruptedException。LockSupport不会像suspend()与resume()一样产生死锁,因为它内部采用了类似信号量的机制。如果许可不可用,将会阻塞。(这里的许可不能叠加)所以即使unpark()发生在park()前,只会阻塞,而不会像锁一样抛出异常。
方法:
public static void park()
public static void unpark()
public static void park(Object blocker)
park()与unpark()是suspend()与resume()的替代。
park():假如线程没有许可证,那么会将线程挂起,假如线程有许可,被调用的话会直接返回。
park(Object blocker)中的blocker是阻塞对象。
park()能支持中断影响,但不会报出异常。
unpark()给给定的线程发放许可,假如线程因为park()而堵塞,那么线程将会不再堵塞,假如线程没有启动,那么这个操作将不会有任何效果,不会发放许可。
8.Guava 和 RateLimiter 限流
限流算法:
1)简单算法:用计数器来统计请求数量,当超过限制后,抛弃剩余请求。
问题:很难控制边界时间上的请求,比如限制了1秒处理十个问题,结果某一秒的后半秒和后一秒的前半秒都执行了十个问题,导致违反了规则。这只是一种简单粗暴的总数量限流,而不是平均限流。
2)漏桶算法:利用一个缓冲区,当有请求进入系统的时候,无论请求的速率如何,都先在缓存区存储,然后以额定流速流出缓冲区。
特点:无论外部请求压力如何,漏桶算法总是能以额定流速处理。漏桶的容积和流出速率是两个重要参数。
3)令牌桶算法
令牌桶算法是一种反向的漏桶算法,桶中存放的不是请求而是令牌,处理程序只有拿到令牌后,才能对请求进行处理。处理程序只有拿到令牌后,才能对请求进行处理;如果没有令牌,那么处理程序要么丢弃令牌,要么等待可用的令牌。为了限制流量,该算法在每个单位时间内产生一定量的令牌丢入桶中。(桶的容量是有限的)
RateLimiter:
包:com.google.common.util.concurrent.RateLimiter
Google-guava并发包里的一个很实用的工具~
guava/下载链接
checkerframework下载链接
error-prone下载链接
j2objc下载链接
animal-sniffer-annotations下载链接
不会maven只能一个个找jar然后放进项目 QAQ
构造方法:
static RateLimiter limiter = RateLimiter.create(2);
方法:
public double acquire()
public boolean tryAcquire()
举例1:控制每秒处理两个请求。用acquire()来控制流量。
public class Test{
static RateLimiter limiter = RateLimiter.create(2);
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
limiter.acquire();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}).start();
}
}
}
举例2:在某些场景中,为了保证服务质量,更倾向于丢弃这些方法,此时可以用tryAcquire()来进行判断。
public class Test{
static RateLimiter limiter = RateLimiter.create(2);
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
if(!limiter.tryAcquire()){
continue;
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}).start();
}
}
}
最终只会输出一个结果,因为进行50次循环的时间远小于等待的500ms。
二.线程复用:线程池
在实际生产环境中,线程的数量必须得到控制,盲目创建大量线程对系统性能是有伤害的。所以我们可以让创建的线程复用,从而避免系统频繁地创建和销毁线程。
1.什么是线程池
在线程池中,总有几个活跃线程。当你需要使用线程时,从池子中获取一个可用的连接即可;当需要关闭连接时,将这个线程退回线程池,供他人使用。
创建线程 --> 从线程池里获取空闲线程
关闭线程 --> 向线程池里归还线程
2.JDK中的线程池框架 Executor
包:java.util.concurrent
设计模式:工厂模式
每个方法都封装在自己的工厂类里并实现ThreadFactory接口(具体代码请看源码)
ThreadPoolExecutor
线程池核心的几个方法内部都实现了ThreadPoolExecutor类,所以只要理解了ThreadPoolExecutor,自然就明白了那些不同线程池方法的意义。
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
函数参数含义如下:
corePoolSize:表示线程池保有的最小线程数。
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当线程池数量超过corePoolSize时,多余的空闲线程的存活时间。(超过即被销毁)
unit:时间单位。
workQueue:任务队列,被提交但未被执行的任务。
threadFactory:线程工厂,用于创建线程。(不会的可以去了解一下工厂设计模式)
handler:拒绝策略。当任务来不及处理的时候,如何拒绝任务。
ThreadPoolExecutor方法:
//提交任务
void execute(Runnable command)
// 提交 Runnable 任务
Future<?> submit(Runnable task);
// 提交 Callable 任务
<T> Future<T> submit(Callable<T> task);
// 提交 Runnable 任务及结果引用
<T> Future<T> submit(Runnable task, T result);
Future方法:
// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否已取消
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);
调度逻辑
伪代码:
任务提交;
if(当前线程数 < corePoolSize)
分配线程执行;
else
{
提交到等待队列;
if(成功提交)
等待执行;
else
{
提交线程池;
if(线程数 >= maximumPoolSize)
拒绝策略;
else
分配线程执行;
}
}
四个workQueue:
1)直接提交的队列 SynchronousQueue
没有容量,每一个插入要等待一个删除操作,每一个删除要等待一个插入操作,提交的任务直接交给线程执行,如果没有空闲的线程,则创建一个新的,如果线程数量到达上限maximumPoolSize,则执行拒绝策略。(所以为了避免频繁执行拒绝策略,一般让maximumPoolSize尽可能大)。
2)有界的任务队列 ArrayBlockingQueue
构造函数必须带一个容量参数,表示最大容量。
若有新的任务需要执行,若实际线程数小于corePoolSize,则会优先创建新的线程,若大于,则会把新任务加入等待队列。若等待队列已满,且总线程数小于maximumPoolSize,则会创建新的线程执行任务;若大于maximumPoolSize,则执行拒绝策略。
有界队列仅当任务队列装满时,才会把线程数提升到corePoolSize以上。
3)无界的任务队列 LinkedBlockingQueue
特殊的ArrayBlockingQueue,队列能被一直添加,直到系统内存耗尽为止。能保证核心线程数不会超过corePoolSize。
4)优先任务队列 PriorityBlockingQueue
特殊的LinkedBlockingQueue,队列出队顺序是按优先级顺序,也就是队列是优先队列。
不同的线程池:
1)newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们可以观察到newFixedThreadPool的corePoolSize与maximumPoolSize是相同的,这也就代表它会返回一个固定线程数量的线程池,该线程池子中的数量始终不变。
keepAliveTime为0,代表一旦线程池数量超过corePoolSize,多余的空闲线程会被立即销毁。
LinkedBlockingQueue代表,它使用无界的队列存放无法立即执行的任务。若是任务提交很频繁,可能会导致系统内存耗尽。
2)newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
该方法返回只有一个线程的线程池,等价于newFixedThreadPool(1)。
3)newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以根据实际情况调整线程池大小的线程池。corePoolSize为0,这代表在没有任务时候,线程池内无线程。由于使用了SynchronousQueue作为任务队列,当有新任务加入队列时,它总会迫线程池增加新的线程执行任务。任务执行完毕后,由于corePoolSize为0,它将会在60秒内被回收。又maximumPoolSize为无限大,所以如果有大量任务被提交,也有可能会导致系统内存耗尽。
4)计划任务方法
newSingleThreadScheduledExecutor() //线程池大小为1
newThreadScheduledExecutor(int corePoolSize, ThreadFactory threadFactory) //线程池大小可指定
和上面三个不同的是,它们可以根据时间需要对线程进行调度。
方法
1)schedule
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
会给定时间对任务进行一次调度command
2)scheduleAtFixedRate
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
3)scheduleWithFixedDelay
会对任务进行周期性的调度,任务调度频率是一定的,是以上个任务开始执行时间为起点,在之后的max(调度周期period,任务执行时间)后调度下一次任务。【也就是说不会出现任务堆叠】
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
会对任务进行周期性的调度,任务调度频率是一定的,是以上个任务结束为起点,在之后的delay时间进行下一次任务。
拒绝策略
当任务超过系统实际承载能力的时候,使用一套机制来合理地处理问题。
JDK内置的四种拒绝策略:
1)AbortPolicy:该策略会直接抛出异常。
2)CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当时被丢弃的任务,但会导致任务提交线程的性能急剧下降。
3)DiscardOldestPolicy:将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交该任务。
4)DiscardPolicy:丢弃无法处理的任务,不进行任何处理
以上内置策略都实现了RejectExecutionHandler接口,所以也可以自己扩展接口写自定义的拒绝策略
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
自定义线程创建 ThreadFactory
ThreadFactory是一个接口,它只有一个用来创建线程的方法
Thread newThread(Runnable r)
我们可以自定义线程池来更加自由地设置线程池中线程的状态。
扩展线程池
ThreadPoolExecutor是一个扩展的线程池,提供了beforeExecute(),afterExecute(),terminated()三个接口。我们可以通过重写这三个接口来实现对线程池运行状态的跟踪以及调试信息的输出。
三.并发容器
时代的眼泪——同步容器
Java 1.5 之前提供的同步容器虽然也能保证线程安全,但是性能很差。使用同步容器的时候,必须用synchronized修饰容器的实例。
构造方法(以hashmap为例)
public statci Map m = Collections.synchronizedMap(new HashMap());
同步容器的实现方法为:
1.内置一个原容器的实例
2.所有访问原容器的方法,增加synchronized关键字
3.增加一个 addIfNotExist() 方法
并发容器
1.List
List 里面只有一个实现类就是CopyOnWriteArrayList
CopyOnWrite,顾名思义就是写的时候会将数组复制一份,写好后,将指针指向新的数组,这样做的好处是读操作完全无锁,并且读写是并行的(读是在原数组上读,写是在新数组上写)。
适用场景:写操作非常少的场景,能够容忍读写的短暂不一致。迭代的时候,不会删改。
2.Map
Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap,它们从应用的角度来看,主要区别在于ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。
ConcurrentSkipListMap 里面的 SkipList 本身就是一种数据结构,中文一般都翻译为“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。
3.Set
Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,使用场景可以参考前面讲述的 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它们的原理都是一样的,这里就不再赘述了。
4.Queue
从以下两个维度来分类。一个维度是阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。Java 并发包里阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识。
1)单端阻塞队列:
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是 LinkedBlockingQueue);甚至还可以不持有队列(其实现是 SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。
而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照优先级出队;DelayQueue 支持延时出队。
2)双端阻塞队列:
LinkedBlockingDeque
3)单端非阻塞队列:
ConcurrentLinkedQueue
4.双端非阻塞队列:
ConcurrentLinkedDeque
四.原子类
JDK包将无锁方案封装成原子类。
将 long 型变量 count 替换为了原子类 AtomicLong,将 count +=1 替换成了 count.getAndIncrement(),就可以实现原子性。(只针对一个变量,解决多个变量的互斥性还是用互斥锁)
实现方案:
CAS指令:
Compare And Swap,包含三个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。计算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着线程在执行完代码之前,count 的值被其他线程更新过。那此时该怎么处理呢?可以采用自旋方案,重新读 count 最新的值来计算 newValue 并尝试再次更新,直到成功。
ABA问题:如果 cas(count,newValue) 返回的值不等于count,意味着线程,count 的值被其他线程更新过,可cas(count,newValue) 返回的值等于count,这也并不代表 count 的值没有被其他线程更新过,因为可能之前变成了其他的值,后来又被修改了回来,这就是 ABA 问题。
解决方法:多传一个版本号。
AtomicStampedReference:加一个int的版本号
AtomicMarkableReference:加一个boolean的版本号
主要方法:
getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta)
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四个方法
// 新值可以通过传入 func 函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)