【小明】谈谈你对Lock和AQS的理解【建议收藏】
2021/9/14 6:07:01
本文主要是介绍【小明】谈谈你对Lock和AQS的理解【建议收藏】,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、直接开始谈Lock
【简单谈一下 Lock使用,实现类源码和原理,点到为止(在说完AQS时可以回过头谈一谈 ReentrantReadWriteLock 原理或者自己设计一个锁或者AQS怎么做),抛出JDK8中新的优化类,证明咱技术还可以,快速过,暂时重点放在后面 AQS 上面】
Lock是JDK提供给我们的显示锁,一般我们在try块前使用 lock() 方法获取锁,finally 块中调用unlock() 释放锁,通常除了获取释放,我们还使用 lockInterruptibly() 可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程;还有 tryLock() 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false;
lock接口中 ReentrantLock 是一个重要的而且我经常用的一个实现类,ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。
/** * Creates an instance of {@code ReentrantLock} with the given fairness policy. * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
除此之外还有 ReentrantReadWriteLock 读写锁,它是实现的是ReadWriteLock接口,一般读多写少的时候会使用它,不过需要注意线程饥饿问题。
【亮点】Java8中的新提供了一个 StampLock,它是读写锁的一个改进版本,第一,它提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程;第二优化了读不阻塞写:就是说在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写,但是写和写之间还是阻塞的。
public class RwLock { private int Integer; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock getLock = lock.readLock();//读锁 private final Lock setLock = lock.writeLock();//写锁 public Integer getNum() { getLock.lock(); try { return this.Integer; // 只读 } finally { getLock.unlock(); } } public void setNum(int number) { setLock.lock(); try { Integer++; // 写入 } finally { setLock.unlock(); } } }
二、开始谈AQS
【先谈AQS的关注点表示你自己知道AQS在Lock体系总的作用,再谈使用、设计模式、数据结构、源码实现等等扩展点,最后可以】
AQS全称AbstractQueuedSynchronizer,意思就是抽象队列同步器,在Lock实现类中大量使用,那我们关注点AQS是如何实现,当我多个线程要等待一把锁的时候,JDK是如何安排这些线程,在AQS中进行等待;当锁被释放的时候,又如何进行唤醒的。
【总】针对关注点,了解到,AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int 成员变量 state 表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
【分】AQS的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态, AQS为保证线程安全,提供的3个方法 getState()、setState(int newState) 和 compareAndSetState(int expect,int update);AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法,来供自定义同步组件使用,AQS既可以支持独占式、共享式地获取同步状态,例如 ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等都在内部使用AQS管理线程。实现者需要继承 AQS 并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的 模板方法,而这些模板方法将会调用使用者重写的方法。
AQS内部方法主要分为,模板方法、可重写的方法和访问或修改同步状态的方法三类,模板方法主要有 acquire(int arg) 方法独占式获取同步状态,release(int arg) 方法独占式的释放同步状态;可重写方法有 tryAcquire(int arg) 独占式获取同步状态,tryRelease() 方法等;访问或修改同步状态的方法就上面说的三种,有 getState() 方法获取当前同步状态。 setState(int newState) 方法设置当前同步状态。和 compareAndSetState(int expect,int update) 方法使用CAS设置当前状态。
AQS中是通过内部类 Node 来维护一个CLH队列 的,CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋;内部节点Node主要 head 和 tail 一个指向队列头节点,而另一个指向队列尾节点;Condition等待队列 ,一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。Condition有两个重要方法 await() 和 signal() , await() 方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁, 同时线程状态变为等待状态。将会以当前线程构造节点,并将节点从尾部加入等待队列。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。调用Condition的 signal() 方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
三、在谈Lock原理
【锁的可重入 原理 ,读写锁原理】
ReentrantLock 锁的可重入 ,是 nonfairTryAcquire() 方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false
ReentrantReadWriteLock 的实现,是读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。写锁一旦被获取,则其他读写线程的后续访问均被阻塞。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态。
四、本文扩展
自己实现一个 AQS、实现一个自己的 独占锁(基于AQS实现锁)、实现一个 可重入锁
【自己实现一个AQS】
应该关心些什么信息?
1、线程信息,肯定要知道我是哪个线程;
2、队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
3、前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)。
import tool.SleepTools; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * 实现一个自己的AQS * 思路:1.定义一个带锁状态的Node * 2.定义一个FIFO阻塞队列,用以存放阻塞的Node * 3.释放锁时,设置锁状态为false,并删除结点 * * @author Cai * @date 2021/8/15-19:49 */ public class CLHLock implements Lock { // 原子读写的对象引用变量,FIFO阻塞队列 AtomicReference<Node> tail; // 线程的前驱结点 ThreadLocal<Node> myPred; // 线程的当前结点 ThreadLocal<Node> myNode; public CLHLock() { // 创建的默认队尾结点,默认是释放锁的状态(locked为false) tail = new AtomicReference<Node>(new Node(false)); myPred = ThreadLocal.withInitial(() -> null); myNode = ThreadLocal.withInitial(() -> new Node(false)); } @Override public void lock() { System.out.println(Thread.currentThread().getName() + " ready get lock"); // 给当前线程 创建一个节点Node 例如:1,2同时进来 Node1,Node2 Node node = myNode.get(); // 当前线程需要获得锁,给当前线程设置状态为true 例如:1,2都为true node.locked = Boolean.TRUE; // 获取取到前趋节点Node pred,然后设置当前线程的节点到队尾 例如:1先到到,tail(false)->1(true)->2(true) Node pred = tail.getAndSet(node); // 设置当前线程的 前趋节点Node pred 例如:1的前驱就是tail(false),tail(false)->1(true) myPred.set(pred); // 一直阻塞,直到前趋节点pred释放锁(pred.locked为false时可以获取锁) while (pred.locked) { } System.out.println(Thread.currentThread().getName() + "---already got lock---"); } @Override public void unlock() { System.out.println(Thread.currentThread().getName() + " ready release lock"); // 获取当前持有锁的 线程的 Node节点 Node node = myNode.get(); // 设置当前线程释放锁的状态为 false node.locked = Boolean.FALSE; // 解除myNode绑定在当前线程上,即使下次当前线程再次需要获得锁时则再创建一个Node。 myNode.remove(); myPred.remove(); System.out.println(Thread.currentThread().getName() + "---already released lock---"); } /** * 模拟AQS */ private static class Node { volatile boolean locked; // 锁状态 Node(boolean locked) {this.locked = locked;} } public static void main(String[] args) { // 创建实现的独占锁 final Lock lock = new CLHLock(); // 创建工作线程 class Worker extends Thread { public void run() { lock.lock(); System.out.println("当前持有锁的线程:" + Thread.currentThread().getName()); try { SleepTools.second(1); // 休眠1s } finally { lock.unlock(); } } } // 启动4个子线程 for (int i = 0; i < 4; i++) { Worker w = new Worker(); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { SleepTools.second(1); } } }
【自己实现一个自己的独占锁(基于AQS实现锁)】
重写AQS经验总结
同步组件的实现依赖同步器,使用AQS的方式被推介定义 继承AQS的静态内部类
AQS采用模板方法设计,AQS的protected方法需要子类重写,当调用模板方法时就可以调用被重写方法
AQS负责同步状态的管理,线程的排队,等待唤醒等底层操作,而自定义同步组件主要专注于 实现同步语义
重写AQS时,使用AQS提供的getState().setState(),compareAndSetState()修改同步状态
import tool.SleepTools; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * 实现一个自己的独占锁 * 思路: 1.实现Lock接口 * 2.使用AQS实现继承自接口中的方法 * * @author Cai * @date 2021/8/15-18:56 */ public class SelfLock implements Lock { // 静态内部类,自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { /*判断处于占用状态*/ @Override protected boolean isHeldExclusively() { // 获取当前同步状态, return getState() == 1; } /*获得锁*/ @Override protected boolean tryAcquire(int arg) { // 使用CAS设置当前状态,该方法能够保证状态设置的原子性 if (compareAndSetState(0, 1)) { // 设置当前拥有独占访问权限的线程 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } /*释放锁*/ @Override protected boolean tryRelease(int arg) { if (getState() == 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); //compareAndSetState(1,0); return true; } // 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } } // 仅需要将操作代理到Sync上即可 private final Sync sync = new Sync(); public void lock() { // 竞争锁的队列,如果没竞争成功,会进入队列 System.out.println(Thread.currentThread().getName() + " ready get lock"); // 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待 sync.acquire(1); System.out.println(Thread.currentThread().getName() + "---already got lock---"); } public boolean tryLock() { // 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 return sync.tryAcquire(1); } public void unlock() { System.out.println(Thread.currentThread().getName() + " ready release lock"); // 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 sync.release(1); System.out.println(Thread.currentThread().getName() + "---already released lock---"); } /** * 等待队列 * 一个Condition包含一个FIFO等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter) * 在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程, * 节点的定义复用了AQS中节点的定义,也就是说,同步队列和等待队列中节点类型都是AQS的静态内部类。 */ public Condition newCondition() { return sync.newCondition(); } public void lockInterruptibly() throws InterruptedException { // 与acquire(int arg)相同.但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则会抛出InterruptedException并返回 sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { // 在acquircInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false.如果获取到了返回true return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public static void main(String[] args) { // 创建实现的独占锁 final Lock lock = new SelfLock(); // 创建工作线程 class Worker extends Thread { public void run() { lock.lock(); System.out.println("当前持有锁的线程:"+Thread.currentThread().getName()); try { SleepTools.second(1); // 休眠1s } finally { lock.unlock(); } } } // 启动4个子线程 for (int i = 0; i < 4; i++) { Worker w = new Worker(); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { SleepTools.second(1); } } } 输出效果: Thread-0 ready get lock Thread-3 ready get lock Thread-0---already got lock--- Thread-2 ready get lock Thread-1 ready get lock 当前持有锁的线程:Thread-0 Thread-0 ready release lock Thread-3---already got lock--- 当前持有锁的线程:Thread-3 Thread-0---already released lock--- Thread-3 ready release lock Thread-3---already released lock--- Thread-2---already got lock--- 当前持有锁的线程:Thread-2 Thread-2 ready release lock Thread-2---already released lock--- Thread-1---already got lock--- 当前持有锁的线程:Thread-1 Thread-1 ready release lock Thread-1---already released lock---
【实现一个可重入锁】
import tool.SleepTools; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @author Cai * @date 2021/8/15-23:27 */ public class ReenterSelfLock implements Lock { // 静态内部类,自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { // 是否处于占用状态 protected boolean isHeldExclusively() { return getState() > 0; } // 当状态为0的时候获取锁 public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } else if (getExclusiveOwnerThread() == Thread.currentThread()) { setState(getState() + 1); return true; } return false; } // 释放锁,将状态设置为0 protected boolean tryRelease(int releases) { if (getExclusiveOwnerThread() != Thread.currentThread()) { throw new IllegalMonitorStateException(); } if (getState() == 0) throw new IllegalMonitorStateException(); setState(getState() - 1); if (getState() == 0) { setExclusiveOwnerThread(null); } return true; } // 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } } // 仅需要将操作代理到Sync上即可 private final Sync sync = new Sync(); public void lock() { System.out.println(Thread.currentThread().getName() + " ready get lock"); sync.acquire(1); System.out.println(Thread.currentThread().getName() + " already got lock"); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { System.out.println(Thread.currentThread().getName() + " ready release lock"); sync.release(1); System.out.println(Thread.currentThread().getName() + " already released lock"); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public static void main(String[] args) { // 创建实现的独占锁 final Lock lock = new ReenterSelfLock(); // 创建工作线程 class Worker extends Thread { public void run() { lock.lock(); System.out.println("当前持有锁的线程:" + Thread.currentThread().getName()); try { SleepTools.second(1); // 休眠1s } finally { lock.unlock(); } } } // 启动4个子线程 for (int i = 0; i < 4; i++) { Worker w = new Worker(); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { SleepTools.second(1); } } }
这篇关于【小明】谈谈你对Lock和AQS的理解【建议收藏】的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享