深入浅出Java 锁 | 源码剖析 | 万字解析
目录
硬件内存结构&Java内存模型
硬件内存结构
Java内存模型(JMM)
JMM中三大特性:原子性、有序性、可见性
Java中有哪些锁?
Java中锁可以分成悲观锁和乐观锁的实现。
乐观锁和悲观锁的区别,乐观锁一定好嘛?
CAS 比较和交换
synchronized锁(互斥锁)
原理
对象在内存中是如何进行存储的?
synchronized锁升级
ReentrantLock(互斥锁)
tips:如果竞争比较激烈,推荐lock锁,效率更高;如果几乎没有竞争,推荐synchronized
基本写法
启动源码分析!
lock方法
tryAcquire方法,尝试获取锁资源
addWaiter方法
acquireQueued方法
unlock方法
为什么唤醒线程时,为啥从尾部往前找?
延伸问题:AQS是什么?【AbstractQueuedSynchronizer】
ReentrantReadWriteLock(读写锁)
发展的产物
启动源码!
写锁——lock方法
写锁——tryAcquire方法
写锁——unlock方法
读锁——lock方法
加锁-扔到队列准备阻塞操作
硬件内存结构&Java内存模型
硬件内存结构
处理器和计算机存储设备之间运算速度相差几个数量级,为了达到“高并发”的效果,在处理器和内存之间加了高速缓存“Cache”。
将运算需要使用的数据复制到缓存中,让运算能够快速进行,当运算完成后,再将缓存中的结果写入主内存中,这样运算器就不用等待主内存的读写操作。 每个处理器都有自己的高速缓存,同时又共同操作同一块主存,当多个处理器同时操作主存时,可能导致数据不一致,因此需要“缓存一致性协议”保障。
Java内存模型(JMM)
说白了,就是CPU内存和JVM内存之间的一个规范,这个规范可以将JVM的字节码指令转换为CPU能够识别的一些指令。
比如在×86的CPU中,原子性的保证要基于cmpxchg(Compare And Exchange)去实现,但是其他的CPU型号各自不同,在JMM中,就会将涉及到的CAS操作根据不同的CPU情况去翻译成不同的指令。
Java内存模型简称JMM(Java Memory Model),用来屏蔽各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能够达到一致的内存访问效果。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存,每个线程都有一个私有的本地内存,本地内存存储了该线程读写变量的副本。
主内存(JVM):主要存储Java实例对象,所有线程创建的实例对象都存放在主存中,不管该实例对象是成员变量还是方法中的局部变量,当然也包括共享的类信息、常量、静态变量。共享数据区域,多个线程对同一个变量访问可能会出现线程安全问题。
工作内存:主要存储当前方法的所有本地变量信息,每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题,例如ThreadLocal。
JMM中三大特性:原子性、有序性、可见性
这里跟MySQL中区分开哦~点击查看MySQL中事务与锁
并发编程原子性:事务是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。在JMM层面,指一个或多个指令在CPU中执行过程中不允许中断。
有序性:指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。
如果不希望CPU对指令进行重排序,可以追加volatile属性进行修饰,就不会对当前属性进行指令重排序。
正常顺序是申请内存,初始化,关联,如果CPU对指令重排,可能会造成申请内存,关联,初始化,在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果。
案例——单例模式中双重校验方式,点击查看博客内容~
可见性:线程之间是隔离的,线程拿到的是主存变量的副本,更改变量需要刷新回主存,其他线程需要从主存重新获取才能拿到变更的值。
Java中有哪些锁?
Java中锁可以分成悲观锁和乐观锁的实现。
乐观锁和悲观锁是俩概念,不是单独指定/指代某个锁。
Java中对这俩种概念做了具体的落地。
-
乐观锁:认为在操作的时候,没有线程与我并打操作,正常的写,但是有并发就会失败,返回flase,成功则返回true;不会阻塞,会等待、失败再试······
-
悲观锁:认为在操作的时候,有线程和我并发操作,就先尝试竞争锁资源,如果拿不到这个锁,就将线程挂起,阻塞等待。
-
乐观锁:CAS
-
悲观锁:synchronized、Lock锁
乐观锁和悲观锁的区别,乐观锁一定好嘛?
乐观锁不会让你的 线程阻塞、挂起 ,可以让CPU一直调度执行竞争这个乐观锁,可以直到成功为止。
悲观锁会在竞争锁资源失败后,直接 挂起、阻塞线程 ,等到锁资源释放后,才可能唤醒这个线程再去竞争锁资源。
核心区别就是 是否会挂起线程 ,因为挂起线程这个操作,线程在用户态时不能这么做,需要从用户态转换到内核态,让OS操作系统去唤醒挂起的线程,这个用户态和内核态的切换就比较耗时。
这么看乐观锁相对悲观锁有一定的优势,但是也不是所有场景都ok。
如果竞争很激烈,导致乐观锁一直失败,那CPU就需要一直去调度他,但是又一直失败,就会有点浪费CPU的资源了。会导致CPU占用率飙高······
在操作系统下,线程就一个阻塞状态BLOCKED,Java中为了更好的排查问题,给线程提供了三种阻塞的状态,BLOCKED,WAITING,TIMED_WAITING……
CAS 比较和交换
线程基于CAS修改数据的方式:先从主内存数据,在修改之前比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃修改。可以认为CAS就是比较和交换,而比较与交换 这个是原子操作。
CAS在Java层面就是Unsafe类中提供的一个native方法,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,自己实现。
CAS的几个点:
-
CAS只能对一个变量的修改实现原子性
-
CAS可能存在ABA问题
-
A线程修改主内存数据从1-2,卡在获取1之后;B线程修改从1-2,√;C线程修改从2-1,√;A执行CAS操作,发现主内存是1,直接修改。但是此1非彼1呀!
-
-
在CAS执行次数过多时候,依旧无法实现数据修改,CPU会一直调度这个线程,造成对CPU性能损耗。【乐观锁】
-
synchronized的实现方式是CAS自旋一定次数以后,如果还不行就挂起线程
-
LongAdder的实现方式:当CAS失败后,将操作的值,存储起来到本地线程,先不共享变量,后续一起添加
-
-
在多核情况下,有lock指令保证只有一个线程在执行当前CAS
synchronized锁(互斥锁)
synchronized锁可以给方法、代码块加锁
-
类锁:基础当前类的Class加锁
-
对象锁:基于this对象加锁
synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!
synchronized锁是基于对象实现的!
原理
对象在内存中是如何进行存储的?
synchronized锁升级
synchronized在jdk1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程(用户态到内核态的转换)
在jdk1.6之前synchronized效率贼低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在jdk1.6将synchronized做优化
锁升级:
-
无锁状态、匿名偏向状态:没有线程拿锁。
-
偏向锁状态:没有线程的竞争,只有一个线程再获取锁资源。 线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
-
查看对象头中的MarkWord里的线程ID,是否是当前线程,如果不是,就CAS尝试改,如果是,就拿到了锁资源。
-
-
轻量级锁:偏向锁出现竞争时,会升级到轻量级锁(触发偏向锁撤销)。 轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
-
查看对象头中的MarkWord里的Lock Record指针指向的是否是当前线程的虚拟机栈,如果是,拿锁执行业务,如果不是CAS,尝试修改,修改他几次,不成,再升级到重量级锁。
-
-
重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。 重量级锁就是线程拿不到锁,就挂起。
-
查看对象头中的MarkWord里的指向的ObjectMonitor,查看owner是否是当前线程,如果不是,扔到ObjectMonitor里的EntryList中,排队,并挂起线程,等待被唤醒。
-
偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向synchronized因为不存在从重量级锁降级到偏向或者是轻量。
synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,stw,才可以撤销,并发偏向锁撤销比较消耗资源 在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作, 为了避免启动时,涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。 如果在开启偏向锁的情况下,查看对象,默认对象是匿名偏向。
编译器优化的结果,出现了下列效果
锁消除:线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。
锁膨胀:在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。
偏向锁会降级到无锁状态吗?会,当偏向锁状态下,获取当前对象的hashcode值,会因为对象头空间无法存储hashcode,导致降级到无锁状态。
ReentrantLock(互斥锁)
tips:如果竞争比较激烈,推荐lock锁,效率更高;如果几乎没有竞争,推荐synchronized
原因:synchronized只有锁升级,当升级到重量级锁后,无法降级到轻量级、偏向锁。
lock锁的使用相对synchronized成本更高。
synchronized是非公平锁,lock是公平+非公平锁
lock锁提供的功能更完善,lock可以使用tryLock指定等待锁的时间
lock锁还提供了lockInterruptibly允许线程在获取锁的期间被中断。
synchronized基于对象实现,lock锁基于AQS+CAS实现
基本写法
public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();lock.lock();try{// 业务代码}finally{lock.unlock();}
}
启动源码分析!
那当然是看lock方法啦,lock方法是如何让当前线程获取到锁资源的?
先区分个概念
-
公平锁
-
非公平锁
lock方法
// 公平锁 FairSync sync的lock方法
final void lock() {acquire(1);
}// 非公平锁 NonfairSync sync的lock方法
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
// 无论公平还是非公平,都会走这里,我们可以认为他是核心
// arg = 1
public final void acquire(int arg) {// 1 调用tryAcquire方法:尝试获取锁资源;拿到锁资源就返回true,结束。如果没有拿到锁资源 if (!tryAcquire(arg) &&// 2 执行后面的方法,先addWaiter方法:将没有获取到锁资源的线程封装为Node对象。// 并且插入到AQS队列的尾部。作为tail// 3 调用acquireQueued方法:查看当前排队的Node节点,是否在队列的前面,如果在前面【head.next】,尝试获取锁资源// 如果不在前面,就尝试将线程挂起,阻塞起来acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
tryAcquire方法,尝试获取锁资源
公平锁情况:
// 公平锁 acquires = args = 1
protected final boolean tryAcquire(int acquires) {// 拿到当前线程final Thread current = Thread.currentThread();// 拿到AQS的stateint c = getState();// 如果state == 0,说明没有线程占用着当前的锁资源if (c == 0) {// 判断是否有线程在排队,如果有线程排队,返回true,配上前面的!,那会直接不执行返回最外层的falseif (!hasQueuedPredecessors() &&// 如果没有线程排队,直接CAS尝试获取锁资源compareAndSetState(0, acquires)) {// 将当前占用这个互斥锁的线程属性设置为当前线程setExclusiveOwnerThread(current);return true;}}// 当前state != 0,说明有线程占用着锁资源// 判断拿着锁的线程是不是当前线程(锁重入)else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 将值设置给statesetState(nextc);// 拿锁成功return true;}return false;
}
非公平锁:
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {// 拿到当前线程!final Thread current = Thread.currentThread();// 拿到AQS的stateint c = getState();// 如果state == 0,说明没有线程占用着当前的锁资源if (c == 0) {// 没人占用锁资源,直接抢一波(不管有没有线程在排队)if (compareAndSetState(0, acquires)) {// 将当前占用这个互斥锁的线程属性设置为当前线程setExclusiveOwnerThread(current);// 返回true,拿锁成功return true;}}// 当前state != 0,说明有线程占用着锁资源// 判断拿着锁的线程是不是当前线程(锁重入)else if (current == getExclusiveOwnerThread()) {// 将state再次+1int nextc = c + acquires;// 锁重入是否超过最大限制// 01111111 11111111 11111111 11111111 + 1// 10000000 00000000 00000000 00000000// 抛出errorif (nextc < 0) throw new Error("Maximum lock count exceeded");// 将值设置给statesetState(nextc);// 返回true,拿锁成功return true;}return false;
}
addWaiter方法
尝试获取锁资源失败了。将当前线程封装为node对象,并且插入到AQS队列的末尾。
// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {// 将当前线程封装为Node对象,mode为null,代表互斥锁 Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure// pred是tail节点Node pred = tail;// 如果pred不为null,有线程正在排队if (pred != null) {// 将当前节点的prev,指定tail尾节点node.prev = pred;// 以CAS的方式,将当前节点变为tail节点if (compareAndSetTail(pred, node)) {// 之前的tail的next指向当前节点pred.next = node;return node;}}// 添加的流程为, 自己prev指向、tail指向自己、前节点next指向我// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列enq(node);return node;
}// enq,无论怎样都添加进入
private Node enq(final Node node) {for (;;) {// 拿到tailNode t = tail;// 如果tail为null,说明当前没有Node在队列中if (t == null) { // 创建一个新的Node作为head,并且将tail和head指向一个Nodeif (compareAndSetHead(new Node()))tail = head;} else {// 和上述代码一致!node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
acquireQueued方法
acquireQueued方法会查看当前排队的Node是否是head的next,如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
如果为1,代表是取消的节点,不能挂起
如果为-1,代表挂起当前线程如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程
// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {// 标识。boolean failed = true;try {// 循环走起for (;;) {// 拿到上一个节点final Node p = node.predecessor();if (p == head && // 说明当前节点是head的nexttryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false// 进来说明拿到锁资源成功// 将当前节点置位head,thread和prev属性置位nullsetHead(node);// 帮助快速GCp.next = null; // 设置获取锁资源成功failed = false;// 不管线程中断。return interrupted;}// 如果不是或者获取锁资源失败,尝试将线程挂起// 第一个事情,当前节点的上一个节点的状态正常!// 第二个事情,挂起线程if (shouldParkAfterFailedAcquire(p, node) &&// 通过LockSupport将当前线程挂起parkAndCheckInterrupt())}} finally {if (failed)cancelAcquire(node);}
}// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 拿到上一个节点的状态int ws = pred.waitStatus;// 如果上一个节点为 -1if (ws == Node.SIGNAL)// 返回true,挂起线程return true;// 如果上一个节点是取消状态if (ws > 0) {// 循环往前找,找到一个状态小于等于0的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将小于等于0的节点状态该为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
unlock方法
释放锁资源:【这里倒是部分公平还是非公平了】
-
将state-1。
-
如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)
// 真正释放锁资源的方法
public final boolean release(int arg) {// 核心的释放锁资源方法if (tryRelease(arg)) {// 释放锁资源释放干净了。 (state == 0)Node h = head;// 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程if (h != null && h.waitStatus != 0)、// 唤醒线程unparkSuccessor(h);return true;}// 释放锁成功,但是state != 0return false;
}
// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {// 获取state - 1int c = getState() - releases;// 如果释放锁的线程不是占用锁的线程,抛异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();// 是否成功的将锁资源释放利索 (state == 0)boolean free = false;if (c == 0) {// 锁资源释放干净。free = true;// 将占用锁资源的属性设置为nullsetExclusiveOwnerThread(null);}// 将state赋值setState(c);// 返回true,代表释放干净了return free;
}// 唤醒节点
private void unparkSuccessor(Node node) {// 拿到头节点状态int ws = node.waitStatus;// 如果头节点状态小于0,换为0if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 拿到当前节点的nextNode s = node.next;// 如果s == null ,或者s的状态为1if (s == null || s.waitStatus > 0) {// next节点不需要唤醒,需要唤醒next的nexts = null;// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 经过循环的获取,如果拿到状态正常的节点,并且不为nullif (s != null)// 唤醒线程LockSupport.unpark(s.thread);
}
为什么唤醒线程时,为啥从尾部往前找?
因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。
如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~
如果从后往前找,肯定可以找到全部的节点。
延伸问题:AQS是什么?【AbstractQueuedSynchronizer】
AQS就是一个抽象队列同步器,abstract queued sychronizer,本质就是一个抽象类。
AQS中有一个核心属性state,其次还有一个双向链表以及一个单向链表。
首先state是基于volatile修饰,再基于CAS修改,同时可以保证三大特性。(原子,可见,有序)
其次还提供了一个双向链表。有Node对象组成的双向链表。
最后在Condition内部类中,还提供了一个由Node对象组成的单向链表。
AQS是JUC下大量工具的基础类,很多工具都基于AQS实现的,比如lock锁,CountDownLatch,Semaphore,线程池等等都用到了AQS。
state是啥:state就是一个int类型的数值,同步状态,至于到底是什么状态,看子类实现。
condition和单向链表?都知道sync内部提供了wait方法和notify方法的使用,lock锁也需要实现这种机制,lock锁就基于AQS内部的Condition实现了await和signal方法。(对标sync的wait和notify)
sync在线程持有锁时,执行wait方法,会将线程扔到WaitSet等待池中排队,等待唤醒
lcok在线程持有锁时,执行await方法,会将线程封装为Node对象,扔到Condition单向链表中,等待唤醒
Condition在做了什么:将持有锁的线程封装为Node扔到Condition单向链表,同时挂起线程。如果线程唤醒了,就将Condition中的Node扔到AQS的双向链表等待获取锁。
ReentrantReadWriteLock(读写锁)
发展的产物
因为ReentrantLock是互斥锁,如果有一个操作是读多写少,同时还需要保证线程安全,那么使用ReentrantLock会导致效率比较低。因为多个线程在对同一个数据进行读操作时,也不会造成线程安全问题。
所以出现了ReentrantReadWriteLock锁:读读操作是共享的,写写操作是互斥的、读写操作是互斥的、写读操作是互斥的。单个线程获取写锁后,再次获取读锁,可以拿到。(写读可重入)单个线程获取读锁后,再次获取写锁,拿不到。(读写不可重入)
ReentrantReadWriteLock还是基于AQS实现的。很多功能的实现和ReentrantLock类似还是基于AQS的state来确定当前线程是否拿到锁资源
按位来玩!之前的博客也有这部分内容哦~点击查看按位的应用【雪花算法】
state表示读锁:将state的高16位作为读锁的标识
state表示写锁:将state的低16位作为写锁的标识
锁重入:
-
写锁重入怎么玩:因为写操作和其他操作是互斥的,代表同一时间,只有一个线程持有着写锁,只要锁重入,就对低位+1即可。而且锁重入的限制,从原来的2^31 - 1,变为了2 ^ 16 -1。
-
读锁重入怎么玩:读锁的重入不能仿照写锁的方式,因为写锁属于互斥锁,同一时间只会有一个线程持有写锁,但是读锁是共享锁,同一时间会有多个线程持有读锁。所以每个获取到读锁的线程,记录锁重入的方式都是基于自己的ThreadLocal存储锁重入次数。
读锁重入的时候就不操作state了?不对,每次锁重入还要修改state,只是记录当前线程锁重入的次数,需要基于ThreadLocal记录。具体的通过源码来看
启动源码!
写锁——lock方法
public void lock() {sync.acquire(1);
}public final void acquire(int arg) {
// 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)// 成功返回// 不成功执行下面方法if (!tryAcquire(arg) &&// addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里// acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
写锁——tryAcquire方法
// 读写锁的写锁,获取流程
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();// 拿到了写锁的低16位标识wint w = exclusiveCount(c);// c != 0:要么有读操作拿着锁,要么有写操作拿着锁if (c != 0) {// (Note: if c != 0 and w == 0 then shared count != 0)// 如果 w == 0,代表没有写锁,拿不到!走了 【有人拿着读锁】// 如果 w != 0,代表有写锁,看一下拿占用写锁是不是当前线程,如果不是,拿不到!走了【有人拿着写锁且不是自己】if (w == 0 || current != getExclusiveOwnerThread())return false;// 到这,说明肯定是写锁,并且是当前线程持有// 判断对低位 + 1,是否会超过MAX_COUNT,超过抛Errorif (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquire// 如果没超过锁重入次数, + 1,返回true,拿到锁资源。setState(c + acquires);return true;}// 到这,说明c == 0// 读写锁也分为公平锁和非公平锁// 公平:看下排队不,排队就不抢了// 走hasQueuedPredecessors方法,有排队的返回true,没排队的返回false// 非公平:直接抢!// 方法实现直接返回falseif (writerShouldBlock() ||// 以CAS的方式,将state从0修改为 1!compareAndSetState(c, c + acquires))// 要么不让抢,要么CAS操作失败,返回falsereturn false;// 将当前持有互斥锁的线程,设置为自己setExclusiveOwnerThread(current);return true;
}// state,高16:读,低16:写
00000000 00000000 00000000 0000000000000000 00000001 00000000 00000000 - SHARED_UNIT00000000 00000000 11111111 11111111 - MAX_COUNT00000000 00000000 11111111 11111111 - EXCLUSIVE_MASK
&
00000000 00000000 00000000 00000001 static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 只拿到表示读锁的高16位。
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
剩下的addWaiter和acquireQueued和ReentrantLock看的一样,都是AQS自身提供的方法
写锁——unlock方法
读写锁的释放操作,跟ReentrantLock一致,只是需要单独获取低16位,判断是否为0,为0就释放成功
// 写锁的释放锁
public final boolean release(int arg) {// 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}// 读写锁的真正释放
protected final boolean tryRelease(int releases) {// 判断释放锁的线程是不是持有锁的线程if (!isHeldExclusively())// 不是抛异常throw new IllegalMonitorStateException();// 对state - 1int nextc = getState() - releases;// 拿着next从获取低16位的值,判断是否为0boolean free = exclusiveCount(nextc) == 0;// 返回trueif (free)// 将持有互斥锁的线程信息置位nullsetExclusiveOwnerThread(null);// 将-1之后的nextc复制给statesetState(nextc);return free;
}
读锁——lock方法
public void lock() {sync.acquireShared(1);
}// 读锁加锁操作
public final void acquireShared(int arg) {// tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1if (tryAcquireShared(arg) < 0)// doAcquireShared 前面没拿到锁,这边需要排队~doAcquireShared(arg);
}// tryAcquireShared方法
protected final int tryAcquireShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();// 拿到stateint c = getState();// 那写锁标识,如果 !=0,代表有写锁if (exclusiveCount(c) != 0 &&// 如果持有写锁的不是当前线程,排队去!getExclusiveOwnerThread() != current)// 排队!return -1;// 没有写锁!// 获取读锁信息int r = sharedCount(c);// 公平锁: 有人排队,返回true,直接拜拜,没人排队,返回false// 非公平锁:正常的逻辑是非公平直接抢,因为是读锁,每次抢占只要CAS成功,必然成功// 这就会出现问题,写操作无法在读锁的情况抢占资源,导致写线程饥饿,一致阻塞…………// 非公平锁会查看next是否是写锁的,如果是,返回true,如果不是返回falseif (!readerShouldBlock() &&// 查看读锁是否已经达到了最大限制r < MAX_COUNT &&// 以CAS的方式,对state的高16位+1compareAndSetState(c, c + SHARED_UNIT)) {// 拿到锁资源成功!!!if (r == 0) {// 第一个拿到锁资源的线程,用first存储firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {// 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数firstReaderHoldCount++;} else {// 不是第一个拿到锁资源的// 先拿到cachedHoldCounter,最后一个线程的重入次数HoldCounter rh = cachedHoldCounter;// rh == null: 我是第二个拿到读锁的!// 或者发现之前有最后一个来的,但是不我,将我设置为最后一个。if (rh == null || rh.tid != getThreadId(current))// 获取自己的重入次数,并赋值给cachedHoldCountercachedHoldCounter = rh = readHolds.get();// 之前拿过,现在如果为0,赋值给TLelse if (rh.count == 0)readHolds.set(rh);// 重入次数+1,// 第一个:可能是第一次拿// 第二个:可能是重入操作rh.count++;}return 1;}return fullTryAcquireShared(current);
}// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {// 拿stateint c = getState();// 现在有互斥锁,不是自己,拜拜!if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;// 公平:有排队的,进入逻辑。 没排队的,过!// 非公平:head的next是写不,是,进入逻辑。 如果不是,过!} else if (readerShouldBlock()) {// 这里代码特别乱,因为这里的代码为了处理JDK1.5的内存泄漏问题,修改过~// 这个逻辑里不会让你拿到锁,做被阻塞前的准备if (firstReader == current) {// 什么都不做} else {if (rh == null) {// 获取最后一个拿到读锁资源的rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) {// 拿到我自己的记录重入次数的。rh = readHolds.get();// 如果我的次数是0,绝对不是重入操作!if (rh.count == 0)// 将我的TL中的值移除掉,不移除会造成内存泄漏readHolds.remove();}}// 如果我的次数是0,绝对不是重入操作!if (rh.count == 0)// 返回-1,等待阻塞吧!return -1;}}// 超过读锁的最大值了没?if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// 到这,就CAS竞争锁资源if (compareAndSetState(c, c + SHARED_UNIT)) {// 跟tryAcquireShared一模一样if (sharedCount(c) == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;cachedHoldCounter = rh; }return 1;}}
}
加锁-扔到队列准备阻塞操作
// 没拿到锁,准备挂起
private void doAcquireShared(int arg) {// 将当前线程封装为Node,当前Node为共享锁,并添加到队列的模式final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {// 获取上一个节点final Node p = node.predecessor();if (p == head) {// 如果我的上一个是head,尝试再次获取锁资源int r = tryAcquireShared(arg);if (r >= 0) {// 如果r大于等于0,代表获取锁资源成功// 唤醒AQS中我后面的要获取读锁的线程(SHARED模式的Node)setHeadAndPropagate(node, r);p.next = null; if (interrupted)selfInterrupt();failed = false;return;}}// 能否挂起当前线程,需要保证我前面Node的状态为-1,才能执行后面操作if (shouldParkAfterFailedAcquire(p, node) &&//LockSupport.park挂起~~parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
相关文章:
深入浅出Java 锁 | 源码剖析 | 万字解析
目录 硬件内存结构&Java内存模型 硬件内存结构 Java内存模型(JMM) JMM中三大特性:原子性、有序性、可见性 Java中有哪些锁? Java中锁可以分成悲观锁和乐观锁的实现。 乐观锁和悲观锁的区别,乐观锁一定好嘛&…...
java流程控制12:流程控制练习
流程控制练习 打印三角型 package com.zheng.struct;public class TestDemo {public static void main(String[] args) {//打印三角形 5行for(int i1;i<5;i){for(int j5;j>i;j--){System.out.print(" ");}for(int j1;j<i;j){System.out.print("*&quo…...
JAVA:ByteBuddy 动态字节码操作库的技术指南
1、简述 ByteBuddy 是一个功能强大的 Java 字节码操作库,可以帮助开发者在运行时动态生成和修改类,而无需直接接触复杂的 ASM API。它被广泛应用于框架开发、AOP(面向切面编程)、代理类生成、性能监控等领域。 2、ByteBuddy 的优…...
C语言学习记录(13)自定义类型:结构体
一、结构体变量的声明、创建和初始化 1.结构体变量的声明 结构体变量我们学操作符的时候就顺带讲了一点了,因为当时讲了结构体成员变量访问操作符.。 结构体变量不像int、float这种内置类型的,一旦创建,系统就知道这是干啥的,结…...
rtthread 软件SPI驱动, 支持mode0~3,MSB,LSB
rtthread的软件模拟SPI用的上层PIN驱动写,由于经过层层封装,时钟频率并不会太高,200MHz的MCU跑不到1MHz的时钟频率。所以最好是在底层就模拟好,给上层用。 头文件 struct io_poSOFT {gpio_type *port;uint16_t pin; }; typedef …...
C++自学笔记——动态创建对象
动态创建对象 1. 什么是动态创建对象? 在学习之前的知识点时,我们知道有静态存储期和自动存储期。 静态存储期的对象在程序的整个生命周期内都存在,全局变量和static修饰的局部变量都属于这一类。自动存储期的对象,这些对象在函…...
35.[前端开发-JavaScript基础]Day12-for循环中变量-华为商城-商品列表-轮播图
for循环中监听函数中打印变量 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"wi…...
详细描述以太坊的gas、gaslimit、gasPrice
目录 一、Gas 是什么? ✅ 简要定义: 🧠 举例理解: 二、Gas Limit 是什么? ✅ 简要定义: 分两种: 举例说明: 三、Gas Price 是什么? ✅ 简要定义: 为什么它重要? 示例: 四、 EIP-1559 后的新机制(伦敦升级) 三个要素: 五、额外技巧(开发实用) 本文…...
【Java】Maven
一、概念 是一个项目管理和构建工具,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建。 二、Maven坐标 <groupId>com.itheima</groupId><artifactId>maven-project01</artifactId>&…...
PageCache
目录 一、PageCache的具体过程 二、具体实现代码 一、PageCache的具体过程 页缓存主要解决的是内存外碎片问题,并且直接和系统调用打交道。 申请过程如下: 当中心缓存中没有内存时,会去页缓存申请一个span结构,要经过下面几步: (1…...
Vue3实战五、面包屑,收缩菜单,高亮暗黑主题切换,全屏功能实现
目录 面包屑,收缩菜单,黑夜白夜样式,全屏功能实现收缩菜单按钮结合pinia功能实现第一步、定义布局配置的数据类型第二步、创建布局状态管理文件第三步、使用布局配置状态第四步、进行展开/收起左侧菜单逻辑第五步、动态切换左侧菜单宽度样式第六步、动态…...
Linux内核设计——(二)进程调度
目录 一、进程调度简介 二、多任务 三、调度器 3.1 I/O消耗型和处理器消耗型进程 3.2 进程优先级 3.3 CFS算法 3.4 实时调度策略 3.5 SCHED_FIFO 3.6 SCHED_RR 3.7 调度器入口 四、上下文切换 4.1 睡眠和唤醒 4.2 need_resched标志 4.3 用户抢占 4.4 内核抢占 一…...
【C++初阶】--- string类模拟实现
1.基础函数 1.1成员函数 成员函数主要是_str、_size、_capacity这三个。npos是size_t 的最大值,用于当作后续成员函数的参数的缺省值。 class string { private:char* _str nullptr;//指向字符串的指针size_t _size 0;//字符串长度size_t _capacity 0;//空间大小static c…...
Pythia 使用说明
Pythia 是一个由非营利研究组织 EleutherAI 开发的开源语言模型套件,专注于透明性和可复现性。它是为了推动自然语言处理(NLP)领域的开放研究而设计,尤其在模型训练过程和性能分析方面提供了详尽的文档和数据。 Pythia 的核心特点…...
python:获取某路径下所有图片的名称
可以使用 Python 的 os 模块或者 pathlib 模块来获取指定路径下所有图片的名称。以下是使用这两种方法实现的代码示例: 使用 os 模块 import osdef get_image_names_os(path):image_extensions (.jpg, .jpeg, .png, .gif, .bmp)image_names []for root, dirs, f…...
一个开源的 VS Code 大模型聊天插件:Light-at
这篇文章是一个开发杂谈。对于有经验的开发者来说,可能这个项目并不算特别复杂或者高技术,只是对我个人来说算一个里程碑,因此写篇杂谈文章记录一下。也许也能给起步者一些参考。 项目地址:https://github.com/HiMeditator/light-…...
图论学习笔记2
请先阅读图论学习笔记 1。 在这篇文章里,我们将继续以前 tarjan 求解的强连通分量和双连通分量,讲解其缩点相关内容。 也会讲解一些特殊的图:基环树与仙人掌图、最小树形图。 缩点 我们知道,将强连通分量、双连通分量缩点之后…...
蓝桥杯备赛---真题训练之15届省赛产品360度展示
题目 介绍 在电子商务网站中,用户可以通过鼠标或手势交互实现 360 度全方位查看产品,提升用户体验。现在需要你设计一个 Pipeline 管道函数,用于控制 360 度展示产品的动画序列,通过管道连接各个动画步骤,使产品以流畅…...
图论:单源最短路(BF算法+迪杰斯特拉算法+spfa算法)
单源最短路 概念 dijkstra实现(解决不了负权值) P3371 【模板】单源最短路径(弱化版) - 洛谷 #include<iostream> #include<vector> #include<cstring> using namespace std;typedef pair<int, int> PII…...
嵌入式学习(35)-TTS语音模块FT-VBM-OS支持ModbusRTU
一、概述 FT-TTS-R-01 (下简简“模块”)是一款可将串口传入的文本信息转成语音播报的控制器。块”可下接收任意字 符或者汉字,并通过 TTS 语音合成功能,清晰、准确、自然的合成并播放音频。该块”还带有 1 路继电器输出࿰…...
【Vue-组件】学习笔记
目录 <<回到导览组件1.项目1.1.Vue Cli1.2.项目目录1.3.运行流程1.4.组件的组成1.5.注意事项 2.组件2.1.组件注册2.2.scoped样式冲突2.3.data是一个函数2.4.props详解2.5.data和prop的区别 3.组件通信3.1.父子通信3.1.1.父传子(props)3.1.2.子传父…...
Github上一些使用技巧(缩写、Issue的Highlight)自用
1. GIthub中的一些缩写 LGTM ! 最近经常看到一些迷之缩写,感觉挺有意思的,但是有时候看到一些没见过的缩写还是有点懵逼,不过缩写确实也是很方便去review,这里就记录汇总一下;顺便加了一些git的基操单词(加…...
【团体程序涉及天梯赛】L1~L2实战反思合集(C++)
实战反思汇总记录 仔细审题,想好再写 L1-104 九宫格 - 团体程序设计天梯赛-练习集 易忽略的错误:开始习惯性地看到n就以为是n*n数组了,实际上应该是9*9的固定大小数组,查了半天没查出来 L1-101 别再来这么多猫娘了!…...
ubuntu下的node.js的安装
安装 node-v22.14.0-linux-x64.tar.xz 的步骤如下: 1. 下载和解压 如果尚未下载文件,可以通过 wget 下载(替换为实际下载链接): wget https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz解压文件&…...
VMware-workstation-full-12.5.2 install OS X 10.11.1(15B42).cdr
手把手虚拟机安装苹果操作系统 VMware_workstation_full_12.5.2 unlocker208 Apple Max OS X(M)-CSDN博客 vcpu-0:VERIFY vmcore/vmm/main/physMem_monitor.c:1180 FILE: FileCreateDirectoryRetry: Non-retriable error encountered (C:\ProgramData\VMware): Cann…...
Linux下创建svn库 和 svn安装与操作
1.介绍 SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS、CVS,它采用了分支管理系统,它的设计目标就是取代CVS。适合中小公司的开发人员不多的项目使用,相比git管理工具更简单. 2.安装svn 2.1 国际惯例 首先看…...
React-04React组件状态(state),构造器初始化state以及数据读取,添加点击事件并更改state状态值
1.React组件状态(state) 组件可以拥有状态(state),它是组件数据的私有部分,可以用来管理动态数据。状态仅适用于类组件,或者使用 React 的 Hook 时可以在函数组件中使用。 注意 组件中render方…...
第3课:MCP协议接口定义与开发实践
MCP协议接口开发实战:从标准化设计到跨语言SDK落地 一、引言:为什么接口标准化是多智能体协作的“刚需” 在多智能体系统中,不同语言开发的智能体、异构服务之间的通信效率往往受制于接口兼容性问题。MCP(Model Context Protoco…...
Perl语言的WebAssembly
Perl语言的WebAssembly:将古老的语言带入新世纪 引言 在编程语言发展的历史长河中,Perl作为一门早期广泛使用的脚本语言,以其灵活性和丰富的文本处理能力而闻名。然而,随着互联网和Web技术的迅猛发展,许多开发者开始…...
[ISP] ISP 中的 GTM 与 LTM:原理、算法与与 Gamma 校正的对比详解
在现代图像信号处理(ISP)流水线中,图像增强是提升视觉质量的核心手段之一。尤其是在高动态范围(HDR)内容、弱光环境或复杂光照条件下,Tone Mapping(色调映射)技术的引入成为关键。To…...
健身管理小程序|基于java微信开发健身管理小程序的系统设计与实现(源码+数据库+文档)
健身管理小程序目录 基于微信开发健身管理小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 小程序端: 后台 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取: 博主介绍:✌️大厂码…...
批量将文本合并成单个文件,支持按文件夹合并文本文档
我们的文件夹中有零零碎碎的多个小的文本文件,这对我们存档记录是非常不方便,不友好的。如果我们能够将多个小的文本文件合并成一个完整的大的文本文件,那不管是在共享还是在存档起来都更加的方便。今天给大家介绍一下如何批量将多个文本文件…...
ROS云课三分钟-差动移动机器人巡逻报告如何撰写-中等报告
评语: 成绩中等(70/100),具体如下: 1. 摘要部分 问题描述: 内容空洞:摘要过于简短,仅简要概述了研究内容和实现方法,未突出研究的创新点或重要性。缺乏细节࿱…...
forms实现推箱子小游戏
说明: forms实现推箱子小游戏 效果图: step0:游戏规则 # 推箱子游戏规则说明## 🎯 游戏目标 - 通过控制角色移动,将所有**棕色箱子(3)**推到**红色目标点(4)**上 - 当所有箱子都变为**绿色(7)**时,即完成当前关卡 - 完成全部关…...
图的储存+图的遍历
图的存储 邻接矩阵 #include <iostream>#include <cstring>using namespace std;const int N 1010;int n, m;int edges[N][N];int main() {memset(edges, -1, sizeof edges);cin >> n >> m; // 读⼊结点个数以及边的个数 for(int i 1; i < m; i)…...
蓝桥杯—数字接龙(dfs+减枝)
一.题目 二.思路 一看就是迷宫问题的变种,从左上角到达右下角,要解决 1.8个方向的方向向量,用dx,dy数组代表方向向量 2.要按照一个规律的数值串进行搜索0,1,2,k-1,0,1…...
Solidity智能合约漏洞类型与解题思路指南
一、常见漏洞类型与通俗解释 1. 重入攻击(Reentrancy) 🌀 通俗解释:就像你去银行取钱,柜台人员先给你钱,然后再记账。你拿到钱后立即又要求取钱,由于账还没记,柜台又给你一次钱,这样循环下去你就能拿走银行所有的钱。 漏洞原理:合约在更新状态前调用外部合约,允许…...
临床 不等于 医学-《分析模式》漫谈52
DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 “Analysis Patterns”的第4章“企业财务观察”有这么一句话: An important point about this model——a reflection of its clinical background 2004(机械…...
鸿蒙应用开发中的 Stack 布局模式
在鸿蒙(HarmonyOS)应用开发中,Stack 布局模式是一种非常灵活的布局方式,类似于其他开发框架中的 StackPanel 或 AbsoluteLayout。它允许子组件按照层级关系进行堆叠,后添加的组件会覆盖在先添加的组件之上。开发者可以通过设置组件的位置、大…...
仿modou库one thread one loop式并发服务器
源码:田某super/moduo 目录 SERVER模块: Buffer模块: Socket模块: Channel模块: Connection模块: Acceptor模块: TimerQueue模块: Poller模块: EventLoop模块&a…...
【AI学习】初步了解Gradio
Gradio 是一个开源的 Python 库,专注于快速构建交互式 Web 界面,特别适用于机器学习模型、数据科学项目或任意 Python 函数的演示与部署。它通过极简的代码实现前后端一体化,无需前端开发经验即可创建功能丰富的应用。以下是 Gradio 的核心特…...
C++11QT复习 (十四)
文章目录 Day9 数据结构学习笔记(2025.04.01)一、C基础快速回顾二、STL(标准模板库)三、常见容器及其对应的数据结构四、容器操作演示1. 基本容器使用2. 异构类型容器 五、迭代器详解特点示例用户自定义结构体访问成员 六、算法库…...
ThreadCache
目录 一、Freelist 二、ThreadCache 三、哈希桶映射规则 一、Freelist 在之前整体框架介绍的时候,我们曾说过ThreadCache是一个哈希桶的结构。每一个桶都要存同一个大小的对象块(即最小块的内存)。 那么我们使用FreeList来…...
c++中同步和异步,阻塞和非阻塞原理以及机制
在C中,同步与异步、阻塞与非阻塞是并发编程中的重要概念,它们描述了程序在执行任务时的行为模式。理解这些概念对于设计高效、响应良好的并发程序至关重要。下面我将详细介绍这些概念的原理和机制。 1. 同步与异步 同步(Synchronous&#x…...
Python项目打包指南:PyInstaller与SeleniumWire的兼容性挑战及解决方案
前言 前段时间做一个内网开发的需求,要求将selenium程序打包成.exe放在内网的win7上运行,在掘金搜了一圈也没有发现相关文章,因此将过程中踩到的坑记录分享一下。 本文涵盖了具体打包操作、不同模块和依赖项的兼容性解决方案,以…...
浅谈微信视频号推荐算法
这次可能会稍微有点干货,但保证不晦涩~ 一、算法推荐的本质:猜你喜欢 vs 社交绑架 视频号的推荐系统本质上在做两件事: 预测你的兴趣:通过你的浏览、点赞、评论、分享等行为,分析你的偏好。满足社交需求&…...
selenium 常用方法
selenium 库的常用方法: 方法说明示例代码webdriver.Chrome()初始化 Chrome 浏览器实例。driver webdriver.Chrome()driver.get(url)访问指定的 URL 地址。driver.get("https://example.com")driver.find_element(By, value)查找第一个匹配的元素。elem…...
springboot中使用async实现异步编程
目录 1.说明 2.实现原理 3.示例 4.总结 1.说明 Async 是 Spring 框架提供的一个注解,用于标记方法为异步执行。被标记的方法将在调用时立即返回,而实际的方法执行将在单独的线程中进行。 Async 注解有一个可选属性:指定要使用的特定线程…...
【2024年蓝桥杯Java B组】省赛真题详细解析
【2024年蓝桥杯Java B组】省赛真题 距离比赛仅剩5天,大多数省份可能完成3-4题即可拿到省奖,2025年想要拿到省奖,需要高效利用时间,重点突破关键知识点和题型。这里以【2024年蓝桥杯Java B组省赛真题】为例,梳理我们最后…...
SQL:DDL(数据定义语言)和DML(数据操作语言)
目录 什么是SQL? 1. DDL(Data Definition Language,数据定义语言) 2. DML(Data Manipulation Language,数据操作语言) DDL和DML的区别 什么是SQL? SQL(Structured …...