AQS和ReentrantLock

张开发
2026/4/8 4:25:47 15 分钟阅读

分享文章

AQS和ReentrantLock
AQS 和 ReentrantLock 详解什么是 AQSAQS (AbstractQueuedSynchronizer)看名字就知道抽象的 队列 同步器。简单来说它就是一个用来处理多线程竞争的工具核心就是两点用队列管理竞争的线程没抢到资源的线程排队等着用状态变量记录资源状态比如锁有没有被占用是个抽象类自己不实现具体逻辑而是让子类去实现其实 AQS 就是一种思想一种规范。它通过一个 FIFO先进先出的等待队列来管理多线程对资源的访问让开发者可以通过继承 AQS 来创建具体的同步器。AQS 主要干两件事操作 state 变量这个变量表示资源是否被占用通过 getState()、setState() 和 compareAndSetState() 这三个方法操作实现排队和阻塞管理线程的入队、出队和阻塞唤醒AQS 的内部机制核心组件状态变量 statevolatile int 类型用来表示资源状态比如 0 表示没被占用1 表示被占用等待队列FIFO 线程等待队列基于 CLH 队列实现CLH 队列这是一个虚拟的双向队列没有实际的队列实例只是通过节点之间的 prev 和 next 指针形成链表节点状态CANCELLED (1)节点被取消了SIGNAL (-1)表示后继节点需要被唤醒CONDITION (-2)节点在条件队列里PROPAGATE (-3)共享模式下用的0初始状态AQS 的同步逻辑获取锁流程线程调用 tryAcquire() 尝试获取锁这个方法由子类实现成功了就直接返回失败了就把自己包装成节点把节点加入等待队列在线程里自旋尝试获取锁直到成功释放锁流程线程调用 tryRelease() 释放锁这个方法也是子类实现释放成功后唤醒等待队列里的下一个节点总结实现 AQS 的关键就是维护两个东西state 变量 一个 FIFO 线程等待队列AQS 的应用Java 里很多同步工具都是基于 AQS 做的比如ReentrantLock可重入锁Semaphore信号量CountDownLatch倒计时门闩ReentrantReadWriteLock可重入读写锁FutureTask异步任务下面我们以 ReentrantLock 为例看看它是怎么用 AQS 的。先学会用 ReentrantLock基本用法// 创建锁ReentrantLocklocknewReentrantLock();// 加锁lock.lock();try{// 临界区代码}finally{// 释放锁lock.unlock();}用 ReentrantLock 实现生产者消费者模式importjava.util.LinkedList;importjava.util.Random;importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassTest{publicstaticfinalReentrantLocklocknewReentrantLock();//锁publicstaticLinkedListIntegerlistnewLinkedList();//缓冲区publicstaticvoidmain(String[]args)throwsInterruptedException{ConditionnotEmptylock.newCondition();//缓冲区不空的信号ConditionnotFulllock.newCondition();//缓冲区不满的信号ProducerproducernewProducer(lock,list,notEmpty,notFull);Producerproducer1newProducer(lock,list,notEmpty,notFull);ConsumerconsumernewConsumer(lock,list,notEmpty,notFull);Consumerconsumer1newConsumer(lock,list,notEmpty,notFull);ThreadthreadnewThread(producer,Producer-1);thread.start();Threadthread1newThread(producer1,Producer-2);thread1.start();Threadthread2newThread(consumer,Consumer-1);thread2.start();Threadthread3newThread(consumer1,Consumer-2);thread3.start();// 等10秒后结束Thread.sleep(10000);System.exit(0);}}classProducerimplementsRunnable{privateReentrantLocklock;privateLinkedListIntegerlist;privateConditionnotEmpty;//缓冲区不空的信号privateConditionnotFull;//缓冲区不满的信号publicProducer(ReentrantLocklock,LinkedListIntegerlist,ConditionnotEmpty,ConditionnotFull){this.locklock;this.listlist;this.notEmptynotEmpty;this.notFullnotFull;}Overridepublicvoidrun(){while(true){lock.lock();try{if(list.size()10){Thread.sleep(500);System.out.println(缓存池已经满了);// 生产者等待缓存区不满的信号notFull.await();}else{Thread.sleep(500);intinewRandom().nextInt(100);list.add(i);System.out.println(生产者Thread.currentThread().getName()生产了i);// 生产者通知消费者,缓冲区有元素了notEmpty.signalAll();}}catch(InterruptedExceptione){thrownewRuntimeException(e);}finally{lock.unlock();}}}}classConsumerimplementsRunnable{privateReentrantLocklock;privateLinkedListIntegerlist;privateConditionnotEmpty;privateConditionnotFull;publicConsumer(ReentrantLocklock,LinkedListIntegerlist,ConditionnotEmpty,ConditionnotFull){this.locklock;this.listlist;this.notEmptynotEmpty;this.notFullnotFull;}Overridepublicvoidrun(){while(true){lock.lock();try{if(list.size()0){Thread.sleep(500);System.out.println(缓存池已经空了);// 消费者等待缓存区不空的信号notEmpty.await();}else{Thread.sleep(500);Integerilist.removeFirst();System.out.println(消费者Thread.currentThread().getName()消费了i);// 消费者通知生产者,缓冲区有空间了notFull.signalAll();}}catch(InterruptedExceptione){thrownewRuntimeException(e);}finally{lock.unlock();}}}}ReentrantLock 是如何实现的ReentrantLock 内部有个 Sync 类它继承了 AQS。Sync 有两个子类NonfairSync非公平锁和 FairSync公平锁。非公平锁的执行流程第一个线程抢锁调用lock.lock()内部调用sync.lock()非公平锁的实现直接用 CAS 把 state 从 0 改成 1成功了就把自己设为锁的拥有者第二个线程抢锁同样调用lock.lock()尝试用 CAS 把 state 从 0 改成 1失败了因为已经被第一个线程占用了进入acquire(1)方法先调用tryAcquire()再试一次还是失败调用addWaiter()把自己包装成节点加入队列进入acquireQueued()自旋等待调用shouldParkAfterFailedAcquire()把前驱节点的状态改成 -1调用parkAndCheckInterrupt()阻塞自己第一个线程释放锁调用lock.unlock()内部调用sync.release(1)调用tryRelease()把 state 减到 0成功后返回 true调用unparkSuccessor()唤醒等待队列里的下一个节点也就是第二个线程第二个线程被唤醒继续自旋这次tryAcquire()成功了把自己设为头节点原来的头节点被垃圾回收公平锁和非公平锁的区别非公平锁一来就直接抢不管队列里有没有人等着性能高但可能有人一直抢不到公平锁严格按队列顺序来保证公平但性能会差点AQS 核心方法解析addWaiter 方法把线程包装成节点加入队列privateNodeaddWaiter(Nodemode){NodenodenewNode(mode);for(;;){NodeoldTailtail;if(oldTail!null){node.setPrevRelaxed(oldTail);if(compareAndSetTail(oldTail,node)){oldTail.nextnode;returnnode;}}else{initializeSyncQueue();}}}acquireQueued 方法线程在队列里自旋等待finalbooleanacquireQueued(finalNodenode,intarg){booleaninterruptedfalse;try{for(;;){// 获取当前节点的前驱节点finalNodepnode.predecessor();// 如果前驱节点是头节点那么就再次的尝试获取锁if(pheadtryAcquire(arg)){setHead(node);p.nextnull;returninterrupted;}// 如果当前线程不应该立即挂起或者前驱节点状态不满足挂起条件if(shouldParkAfterFailedAcquire(p,node))interrupted|parkAndCheckInterrupt();}}catch(Throwablet){cancelAcquire(node);if(interrupted)selfInterrupt();throwt;}}unparkSuccessor 方法唤醒下一个等待的线程privatevoidunparkSuccessor(Nodenode){intwsnode.waitStatus;if(ws0)node.compareAndSetWaitStatus(ws,0);Nodesnode.next;if(snull||s.waitStatus0){snull;for(Nodeptail;p!nodep!null;pp.prev)if(p.waitStatus0)sp;}if(s!null)LockSupport.unpark(s.thread);}AQS 的设计亮点模板方法模式核心逻辑自己写具体同步逻辑交给子类复用性高CAS 操作无锁化更新状态性能好CLH 队列高效管理等待线程自旋 阻塞减少线程切换开销可重入性支持锁的重入方便使用总结AQS 是 Java 并发的核心它通过状态变量和等待队列实现了高效的线程同步。ReentrantLock 就是基于 AQS 实现的一个典型例子。理解 AQS 不仅能帮你更好地使用并发工具还能让你在设计自己的同步组件时借鉴它的思想。简单来说AQS 就是一个框架它提供了一套模板让你可以方便地实现各种同步器。掌握了它你对 Java 并发的理解就会上一个台阶。

更多文章