AQS - AbstractQueuedSynchronizer
AQS是并发基类 , 通过State
以及Exclussive Thread
来控制资源总数以及资源独占的线程. 通过LockSupport.park/unpark
来控制线程CPU的调度 , 用于让某个线程获取/让出CPU资源.
AQS主要分为两种模式 :
- 独占模式(Exclussive Mode) : 只有
1
个资源 , 同一时刻只允许一个线程执行- 包括 : ReentrantLock
- 共享模式(Shared Mode) : 拥有多个资源 , 只要资源数足够 , 同一时刻允许多个线程获取资源执行
- 包括 : Semaphore、CountDownLatch、CyclicBarrier
LinkedBlockingQueue
BlockingQueue主要用于实现生产者-消费者模型的实例.
主要通过ReentrantLock
与Condition
来实现生产与消费. 其内部会创建两个ReentantLock :
- takeLock : 负责生成
notEmpty
的Condition , 用于阻塞take
函数获取数据 - putLock : 负责生成
notFull
的Condition , 用于阻塞put
函数插入数据
内部也有capacity
变量用于标志当前队列总长度 , 也有AtomicInteger count
来标志当前队列长度.
当put
函数插入数据 , 而队列满了的时候 , 会通过notFull.await
让插入线程等待. 而当take
函数获取完数据 , 队列不满的时候 , 会通过notFull.signal
唤醒线程进行插入.
当take
函数获取数据, 而队列长度为0的时候 , 会通过notEmpty.await
等待数据插入. 而当put
函数调用后 , 队列中有数据时 , 会通过notEmpty.signal
唤醒线程获取数据.
CAS操作
CAS(Compare and Swap),即比较并交换。比较的是当前内存值与预期值,交换的是新值与内存中的值。这个操作是通过cmpxchg
指令来完成 , 但在该指令前 , 还有一个lock
前缀 , 而这个lock
前缀才是保证多核CPU的关键.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
// 如果是多核CPU , 则lock
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
而在上层 , Unsafe会通过自旋的方式一直等待cmpxchg
执行成功后返回结果.
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
volatile关键字实现
volatile
关键字主要有两个功能 :
- 线程间可见性 : 保证该变量的值在各个线程间都是获取最新的
- 阻止指令重排序 : 保证在
volatile
前的指令可以执行完毕
在class
文件中 , 仅仅只是加上了ACC_VOLATILE
的Flag
在虚拟机中 , 会通过lock
前缀 , 来标志该条指令.
lock
指令用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。
synchronized关键字实现
在jdk1.5之前 , synchronized
关键字的性能非常差 , 因为在JVM中遇到synchronized
关键字就会使用Linux的Mutex
进行并发.
在jdk1.6版本后 , 通过对锁对优化 , synchronized
的性能已经达到一个很优秀的程度. 通过在JVM
加入了偏向锁
、轻量级锁
、重量级锁
的策略.
Java中锁膨胀的顺序为 : 无锁
, 偏向锁
, 轻量级锁
, 重量级锁
- 偏向锁 :
- 检查
对象头
的Mark Word
中是否保存有线程ID , 如果有则认为当前锁处于偏向锁 - 如果没有则通过
CAS
设置对象头中的线程ID , 如果成功 , 则代表从无锁成为偏向锁 - 如果CAS失败或者已经存在线程ID , 则当到线程安全时 , 会撤销偏向锁 , 升级成为轻量级锁
- 轻量级锁 :
多个线程竞争偏向锁导致偏向锁升级为轻量级锁
- JVM 在当前线程的栈帧中创建 Lock Reocrd,并将对象头中的 Mark Word 复制到 Lock Reocrd 中。(Displaced Mark Word)
- 线程尝试使用 CAS 将对象头中的
Mark Word
替换为指向Lock Reocrd
的指针。 - 如果成功则获得锁,如果失败则先检查对象的
Mark Word
是否指向当前线程的栈帧如果是则说明已经获取锁,否则说明其它线程竞争锁则膨胀为重量级锁。
- 重量级锁 :
在重量级锁中没有竞争到锁的对象会park
被挂起,退出同步块时unpark
唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。
synchronized 关键字及 wait、notify、notifyAll 这三个方法都是通过MonitorEnter
、MonitorExit
来实现的.
![](https://img.haomeiwen.com/i1941624/553298f60d23bc22.png)
LockSupport实现
在JVM中 , 会通过Parker
来完成park/unpark
. 实际上 , 对应在Linux上的实现则也是通过mutex
来实现的:
- park :
pthread_cond_wait
以及pthread_mutex_lock
实现等待 - unpark :
pthread_cond_signal
以及pthread_mutex_lock
实现唤醒
缓存一致性
对于当代多核CPU而言 , CPU会有缓存一致性协议 (MESI) , 主要用于标示CPU Cache Line中缓存的状态 , 并且用于同步多个CPU中缓存状态 .
而在CPU指令中Lock
指令用来同步各个CPU核之间的缓存数据.
乐观锁/悲观锁
乐观锁 : 当并发量级很小, 冲突小时 , 使用乐观锁 , 比如volatile
、CAS
操作
悲观锁 : 当并发量级比较大, 冲突较大时 , 使用悲观锁 , 比如ReentrantLock
、synchronized
, 主要因为CAS自旋多次后 , 会导致CPU大量的空旋 , 浪费cpu资源.
网友评论