在Java 1.5
之前,共享变量的互斥访问一般是通过synchronized
代码块来实现,synchronized
在Java
早期只有重量级锁模式,在并发不是很高的环境下性能比较低,并且synchronized
作为一个非公平锁,可能造成一些线程一直处于饥饿状态,也不支持主动的释放锁。因此从Java 1.5
起引入了Java.util.concurrent
并发包,在concurrent
包中提供了一些实用的工具类来支持Java
并发编程。
Semaphore:用于限制同时执行某个操作的线程数的同步辅助类。
CountDownLatch:用于等待其它线程完成操作的同步辅助类。
CyclicBarrier:一种同步辅助类,允许一组线程互相等待,直到所有线程都达到一个共同的屏障点。
ReentrantLock:可重入的互斥锁,用于控制多个线程对共享资源的访问。
ConcurrentHashMap:线程安全的哈希表,用于高并发环境下的 Map 操作。
CopyOnWriteArrayList:线程安全的列表,适合读多写少的场景。
BlockingQueue:阻塞队列,提供了线程安全的队列操作。
基本概念
在理解AQS
之前,要先了解一下AQS
框架中必要的技术概念:
- 自旋锁
自旋是指当线程获取锁时,如果锁已经被独占,线程不会立即进入阻塞状态,而是会一直尝试去获取锁,直到获取锁成功或者超过尝试次数进入阻塞状态。
线程自旋获取锁成功的依据是通过CAS
返回的值来判断,一般是返回true
表示加锁成功。
自旋锁是一种轻量级锁,相比传统的synchronized
锁,在并发竞争不激烈的情况下,可以避免线程频繁的阻塞和唤醒,减少系统的开销。但是在并发竞争激烈的情况下,自旋会导致线程一直尝试获取锁,浪费大量的CPU
时间,性能可能不及重量级锁。
synchronized经过多个版本的优化,已经引入了轻量级锁(也是一种自旋锁),性能并不差。
- 原子操作
CAS
CAS(compare and swap)
是一种原子操作,它有三个参数: 内存位置(V)、预期原值(A)和新值(B),比较预期值与内存位置(V)的值,如果相等,则将该地址处的值设置为新值(V),否则不做任何操作。
在AQS
中,无锁状态0
,大于0
就说明锁被已经被其他线程持有。当线程去加锁时可以拿着0
去比较内存中的锁状态,如果想等,则将内存中的值加1
,加锁成功;如果不相等,说明已经有其他的线程修改了锁状态值,表示加锁失败。CAS
比较、赋值两个步骤是原子性的。
//自旋例子
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//cas 操作
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
AQS类结构
Java AQS (AbstractQueuedSynchronizer)
是Java
并发包中的一个抽象类,它使用一个volatile
变量state
来表示同步的状态(锁是否占用),每次只能一个线程占用这个状态资源进行操作,同时通过一个同步等待队列来管理没有获取到同步状态的线程。
AQS 获取锁流程
AQS
定义了一套加锁、释放锁的流程,继承自AQS
的工具类基本会遵守这套逻辑,如下是加锁逻辑:
// 1.尝试获取锁
if (!tryAcquire()){
for(;;){
//2.将获取锁失败的线程添加到等待队列
acquireQueued();
}
}
-
尝试获取锁:
通过调用cas
来设置state
锁状态,如果设置成功则说明线程加锁成功。 -
加锁失败的线程添加到
AQS
等待队列:
通过自旋cas
将线程追加到等待队列的末尾,线程进入阻塞状态,并且等待它的前置节点唤醒它。