并发编程的基础知识
Java中的CAS操作
在Java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度的开销。
Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,不能解决读-改-写等的原子性问题。CAS即Compare And Swap,
是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
compareAndSwap*
JDK的Unsafe类提供了一系列的compareAndSwap*方法,下面以compareAndSwapLong为例 进行简单介绍。
- boolean compareAndSwapLong(Object obj, long valueOffset, long except, long update)
CAS有四个操作数,分别是:对象内存位置,对象中的变量的偏移量,变量的预期值,更新之后的值。其操作含义:如果对象obj中内存偏移量为valueOffset的变量值为except,
则使用新的值update替换旧的值except。这是处理器提供的一个原子性指令。
CAS自旋等待
CAS自旋,一直尝试,直到成功。
1 | public final long getAndAddLong(Object obj, long valueOffset, long add) { |
ABA问题
假如线程I使用CAS修改初值值的变量X,那么线程I会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功可,那么此时程序运行不一定正确。
可能在线程I获取变量X的值A后,在执行CAS前,线程II使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。虽然线程I执行CAS时的值是A,但是这个A已经不是线程I获
取时的A了。
ABA问题的产生是因为变量的状态值产生了环形转换,A-B-A。JDK中的AtomicStampedReference类为每个变量的状态值都配备了一个时间戳,从而避免ABA问题的产生。
什么是AQS
AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int state)和一个先进先出(FIFO)的线程等待队列来实现一个多线程
访问共享资源的同步框架。
AQS原理
AQS为每个共享资源都设置了一个共享资源锁,线程在需要访问共享资源时首先要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程
放入线程等待队列,等待下一次资源调度。许多同步类的实现都依赖于AQS,比如常用的ReentrantLock、countDownLatch。
state: 状态
Abstract Queued Synchronizer维护了一个volatile int类型的变量,用于表示当前的同步状态。volatile虽然不能保证操作的原子性,但是能保证当前变量state的可见性。
1 | // 返回共享资源状态,此操作的内存语义为volatile修饰的原子读操作 |
AQS共享资源的方式:共享式和独占式
- 共享式:多个线程可以同时执行,具体的实现Semaphore和CountDownLatch。
- 独占式:只有一个线程能执行,具体的实现ReentrantLock。
AQS只是定义一个框架,只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争用贡献资源的方式也不同,自定义同步器在实现时只需要实现共享
资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器再做处理。
方法名 | 资源共享方式 | 说明 |
---|---|---|
isHeldExclusively() | 查询该线程是否正在独占资源,只有用到condition才需要去实现它 | |
tryAcquire(int) | 独占方式 | 尝试获取资源:成功返回true,失败返回false |
tryRelease(int) | 独占方式 | 尝试释放资源:成功返回true,失败返回false |
tryAcquireShared(int) | 共享方式 | 尝试获取资源:负数表示失败;0表示成功,但没有剩余资源可用;正数表示成功,且有剩余资源 |
tryReleaseShared(int) | 共享方式 | 尝试释放资源:如果释放资源后允许唤醒后续等待线程,则返回true,否则返回false |
一般来说,自定义同步器要么采用独占方式,要么采用共享方式,实现类只需实现tryAcquire、tryRelease或tryAcquireShared、tryReleaseShared中的一组即可。但AQS也支持
自定义同步器同时实现独占和共享两种方式,例如ReentrantReadWriteLock在读取时采用共享方式,在写入时采用独占方式。
ReentrantLock
ReentrantLock中的state初始值为0,表示无锁状态。在线程执行tryAcquire()获取该锁后,ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过
tryAcquire()获取锁时均会失败,直到该线程释放锁后,state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1,因此
ReentrantLock也属于可重入锁。但是获取多少次锁就要释放多少次锁,这样才能保证state最终为0。
CountDownLatch
CountDownLatch将任务分为N个子线程去执行,将state也初始化为N,N与线程的个数一致,N个子线程是并行执行,每个子线程都在执行完后countDown()一次,state会执行CAS操作
并减1。在所有子线程都执行完后(state = 0)时会unpark()主线程,然后主线程会从await()返回,继续执行后续的动作。