目录
并发工具-J.U.C
AQS原理
概述
AQS(AbstractQueuedSynchronizer)是java中用于构建锁和同步器的抽象基类。它提供了一种基于FIFO等待队列的锁和同步器的实现框架,是并发编程中实现锁和同步器的重要基础。
AQS提供了两种基本的同步原语:独占锁和共享锁。独占锁是指同一时刻只能有一个线程持有的锁,典型的代表是ReentrantLock。共享锁是指同一时刻可以被多个线程持有的锁,典型的代表是 CountDownLatch 和 Semaphore。
特点:
1用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
子类主要实现这样一些方法
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
实现不可重入锁
自定义同步器
final class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(arg == 1){
if (compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(arg == 1){
if(getState()==0){
throw new IllegalArgumentException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
return false;
}
protected Condition newCondition(){
return new ConditionObject();
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
自定义锁
class MyLock implements Lock {
static MySync sync = new MySync();
@Override
//尝试获得锁,不成功,进入等待队列
public void lock() {
sync.acquire(1);
}
@Override
//尝试,不成功,进入等待队列,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
//尝试一次获得锁,不成功直接返回,不进入等待队列
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
//尝试获得锁,不成功进入等待队列,有时限
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
//释放锁
public void unlock() {
sync.release(1);
}
@Override
//生成条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
测试
public class Test3 {
public static void main(String[] args) {
//测试
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try{
log.debug("获得锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁");
}
},"t1").start();
new Thread(()->{
lock.lock();
try{
log.debug("获得锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁");
}
},"t2").start();
}
}
09:05:06.448 [t1] DEBUG com.example.com.yunlong.test3.Test3 - 获得锁
09:05:07.470 [t1] DEBUG com.example.com.yunlong.test3.Test3 - 释放锁
09:05:07.470 [t2] DEBUG com.example.com.yunlong.test3.Test3 - 获得锁
09:05:08.476 [t2] DEBUG com.example.com.yunlong.test3.Test3 - 释放锁
ReentrantLock原理
非公平锁的加锁实现原理
1.先从构造器来看,默认为非公平锁实现
public ReentrantLock() {
sync = new NonfairSync();
}
2.NonfairSync 继承自 AQS
public void lock() {
sync.lock();
}
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
3.没有竞争时,if语句块直接成立
4.有竞争时,进入acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
Thread-1 执行了
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入tryAcquire逻辑,这时state已为1,结果失败
- 接下来进入addWaiter逻辑,构造Node队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
当前线程进入 acquireQueued 逻辑
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
非公平锁的解锁实现原理
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
Thread-0 释放锁,进入 tryRelease 流程,如果成功
- 设置 exclusiveOwnerThread 为 null
- state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 回到 Thread-1 的 acquireQueued 流程
如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
锁重入原理
static final class NonfairSync extends Sync {
// ...
// Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
公平锁的实现原理
protected final boolean tryAcquire(int acquires){
final Thread current = Thread currentThread();
int c = getState();
if (c == 0){
//先检查AQS队列中是否有前驱节点,没有才去竞争
if(!hasQueuedPrecessors()) && compareAndSetState(0,acquires){
setExclusiveOwnThread(current);
return true;
}
}
else ...
}
读写锁ReentrantReadWriteLock
当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
public class Test4 {
private static ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public static void main(String[] args) {
new Thread(()->{
log.debug("获得读锁");
rw.readLock().lock();
try{
Thread.sleep(1000);
log.debug("开始读取");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rw.readLock().unlock();
}
},"t1").start();
new Thread(()->{
log.debug("获得读锁");
rw.readLock().lock();
try{
Thread.sleep(1000);
log.debug("开始读取");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rw.readLock().unlock();
}
},"t2").start();
}
}
17:07:02.318 [t1] DEBUG com.example.com.yunlong.test3.Test4 - 获得读锁
17:07:02.318 [t2] DEBUG com.example.com.yunlong.test3.Test4 - 获得读锁
17:07:03.334 [t1] DEBUG com.example.com.yunlong.test3.Test4 - 开始读取
17:07:03.334 [t2] DEBUG com.example.com.yunlong.test3.Test4 - 开始读取
结论:
- 读锁-读锁可以并发
- 读锁-写锁相互阻塞
- 写锁-写锁相互阻塞
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取读锁
读写锁StampedLock
该类自JDK8加入之后,是为了进一步优化读性能,它的特点是在使用读锁,写锁时,都必须配合戳使用。
示例
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped{
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data){
this.data = data;
}
public int read(int readTime) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking{}",stamp);
Thread.sleep(readTime);
if(lock.validate(stamp)){
log.debug("read finish...{},data...{}",stamp,data);
return data;
}
//锁升级
log.debug("update to read lock...{}",stamp);
try{
stamp = lock.readLock();
log.debug("read lock{}",stamp);
Thread.sleep(readTime);
log.debug("read finish...{},data...{}",stamp,data);
return data;
}finally {
log.debug("read unlock...{}",stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData){
long stamp = lock.writeLock();
log.debug("write lock...{}",stamp);
try{
Thread.sleep(2000);
this.data = newData;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
log.debug("write lock...{}",stamp);
lock.unlockWrite(stamp);
}
}
}
测试读-读可以优化
public class Test5 {
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainerStamped = new DataContainerStamped(100);
new Thread(()->{
try {
dataContainerStamped.read(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1").start();
Thread.sleep(500);
new Thread(()->{
try {
dataContainerStamped.read(0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t2").start();
}
}
17:38:49.607 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:38:50.119 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:38:50.119 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...256,data...100
17:38:50.622 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...256,data...100
测试 读-写 时优化读补加读锁
public class Test5 {
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainerStamped = new DataContainerStamped(100);
new Thread(()->{
try {
dataContainerStamped.read(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1").start();
Thread.sleep(500);
new Thread(()->{
dataContainerStamped.write(110);
},"t2").start();
}
}
17:41:11.624 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:41:12.132 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - write lock...384
17:41:12.650 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - update to read lock...256
17:41:14.134 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - write lock...384
17:41:14.134 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read lock513
17:41:15.149 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...513,data...110
17:41:15.149 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read unlock...513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
根据不同的需求选择适应的读写锁
Semaphore(信号量)
Semaphore(信号量)是一个经典的同步工具,用于控制同时访问某个特定资源的线程数量。它维护了一个许可证(permit)的计数器,线程在访问资源之前必须先获取许可证,访问完成后必须释放许可证。Semaphore可以用来实现线程的互斥访问,也可以用来实现资源的控制和限流。
Semaphore提供了两种主要的操作:
- acquire(): 获取一个许可证,如果没有许可证可用,则线程将阻塞直到有许可证可用。
- release(): 释放一个许可证,归还给信号量。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final int MAX_AVAILABLE = 5; // 最大可用许可证数量
private static Semaphore semaphore = new Semaphore(MAX_AVAILABLE, true); // 创建信号量,第二个参数设置为true表示公平性
public static void main(String[] args) {
// 创建多个线程并启动
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + " is accessing the resource.");
// 模拟访问资源的耗时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
}
}, "Thread-" + i).start();
}
}
}
在这个示例中,Semaphore被用来控制同时访问资源的线程数量。Semaphore的初始许可证数量为5,意味着最多同时允许5个线程访问资源。当线程尝试获取许可证时,如果当前已经达到了许可证的上限,则线程会被阻塞,直到有其他线程释放许可证为止。通过Semaphore,可以有效地控制并发访问的数量,实现资源的限流
CountDownLatch(倒计时门闩)
CountDownLatch(倒计时门闩)是Java中的一个同步工具,它可以用来实现线程之间的等待机制。它内部维护了一个计数器,初始值为一个正整数,表示需要等待的线程数量。每个线程在完成自己的任务后,可以调用countDown()方法将计数器减一,当计数器的值减至零时,所有等待的线程都会被唤醒
CountDownLatch 提供了两个主要的方法:
- countDown(): 将计数器减一,表示一个线程已经完成了任务。
- await(): 等待计数器的值减至零,阻塞当前线程直到计数器的值为零。
示例
import java.util.concurrent.CountDownLatch;
public class CountdownLatchExample {
private static final int THREAD_COUNT = 5;
private static final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) throws InterruptedException {
// 创建多个线程并启动
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
// 模拟线程执行任务的耗时操作
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " has finished its task.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 每个线程执行完成后将计数器减一
}
}, "Thread-" + i).start();
}
// 等待所有线程执行完成
latch.await();
System.out.println("All threads have finished their tasks.");
}
}
在这个示例中,CountDownLatch的初始计数器值为5,代表了需要等待的线程数量。每个线程在执行完任务后,都会调用countDown()方法将计数器减一。主线程调用await()方法进入等待状态,直到所有线程执行完毕,计数器减至零,主线程被唤醒。通过CountDownLatch,可以实现线程之间的协作,等待所有线程执行完特定任务后再继续执行后续操作。
应用:可以配合线程池使用。相比于join(),属于高级工具。
- 同步等待多线程准备完毕
- 同步等待多个远程调用结束
CyclicBarrier(循环栅栏)
CyclicBarrier(循环栅栏)是Java中的一个同步工具,它可以让一组线程互相等待,直到所有线程都到达了某个同步点之后,才继续执行。与CountDownLatch不同的是,CyclicBarrier的同步点是可重复利用的,一旦所有线程都到达了同步点,栅栏就会打开,所有线程都会被释放,并且栅栏重新初始化,可以再次使用。
CyclicBarrier提供了以下主要的方法:
- await(): 让当前线程等待,直到所有线程都到达了同步点。
- await(long timeout, TimeUnit unit): 让当前线程等待一段时间,如果超时则继续执行。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 5;
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("All threads have reached the barrier.");
});
public static void main(String[] args) {
// 创建多个线程并启动
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
// 模拟线程执行任务的耗时操作
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " has reached the barrier.");
barrier.await(); // 等待其他线程到达栅栏
System.out.println(Thread.currentThread().getName() + " continues its work.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Thread-" + i).start();
}
}
}
在这个示例中,CyclicBarrier的初始参与线程数量为5,当所有线程都调用了await()方法到达了栅栏时,栅栏就会打开,所有线程都会被释放,并且会执行给定的回调函数。在回调函数中,可以执行一些额外的逻辑,比如输出一条提示信息。通过CyclicBarrier,可以实现多个线程之间的同步,并在特定点集合执行任务。