应届毕业生简历模板/seo工作是什么意思
哈喽大家好,我是阿Q。
最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock
加锁解锁的全过程。系好安全带,发车了。
简单使用
在聊它的源码之前,我们先来做个简单的使用说明。当我在IDEA
中创建了一个简单的Demo
之后,它会给出以下提示
提示文字
在使用阻塞等待获取锁的方式中,必须在try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁。
- 1、如果在
lock
方法与try
代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。 - 2、如果
lock
方法在try
代码块之内,可能由于其它方法抛出异常,导致在finally
代码块中,unlock
对未加锁的对象解锁,它会调用AQS
的tryRelease
方法(取决于具体实现类),抛出IllegalMonitorStateException
异常。 - 3、在
Lock
对象的lock
方法实现中可能抛出unchecked
异常,产生的后果与说明二相同。
java.concurrent.LockShouldWithTryFinallyRule.rule.desc
还举了两个例子,正确案例如下:
Lock lock = new XxxLock();
// ...
lock.lock();
try {doSomething();doOthers();
} finally {lock.unlock();
}
错误案例如下:
Lock lock = new XxxLock();
// ...
try {// 如果在此抛出异常,会直接执行 finally 块的代码doSomething();// 不管锁是否成功,finally 块都会执行lock.lock();doOthers();} finally {lock.unlock();
}
AQS
上边的案例中加锁调用的是lock()
方法,解锁用的是unlock()
方法,而通过查看源码发现它们都是调用的内部静态抽象类Sync
的相关方法。
abstract static class Sync extends AbstractQueuedSynchronizer
Sync
是通过继承AbstractQueuedSynchronizer
来实现的,没错,AbstractQueuedSynchronizer
就是AQS
的全称。AQS
内部维护着一个FIFO
的双向队列(CLH
),ReentrantLock
也是基于它来实现的,先来张图感受下。
Node 属性
//此处是 Node 的部分属性
static final class Node {//排他锁标识static final Node EXCLUSIVE = null;//如果带有这个标识,证明是失效了static final int CANCELLED = 1;//具有这个标识,说明后继节点需要被唤醒static final int SIGNAL = -1;//Node对象存储标识的地方volatile int waitStatus;//指向上一个节点volatile Node prev;//指向下一个节点volatile Node next;//当前Node绑定的线程volatile Thread thread;//返回前驱节点即上一个节点,如果前驱节点为空,抛出异常final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}
}
对于里边的waitStatus
属性,我们需要做个解释:(非常重要)
- CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化;
- SIGNAL(-1):后面节点等待当前节点唤醒;
- CONDITION(-2):
Condition
中使用,当前线程阻塞在Condition
,如果其他线程调用了Condition
的signal
方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁; - PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
- 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成;
AQS 属性
// 头结点
private transient volatile Node head;// 尾结点
private transient volatile Node tail;//0->1 拿到锁,大于0 说明当前已经有线程占用了锁资源
private volatile int state;
今天我们先简单了解下AQS
的构造以帮助大家更好的理解ReentrantLock
,至于深层次的东西先不做展开!
加锁
对AQS
的结构有了基本了解之后,我们正式进入主题——加锁。从源码中可以看出锁被分为公平锁和非公平锁。
/*** 公平锁代码*/
final void lock() {acquire(1);
}/*** 非公平锁代码*/
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
初步查看代码发现非公平锁似乎包含公平锁的逻辑,所以我们就从“非公平锁”开始。
非公平锁
final void lock() {//通过 CAS 的方式尝试将 state 从0改为1,//如果返回 true,代表修改成功,获得锁资源;//如果返回false,代表修改失败,未获取锁资源if (compareAndSetState(0, 1))// 将属性exclusiveOwnerThread设置为当前线程,该属性是AQS的父类提供的setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
compareAndSetState()
:底层调用的是unsafe
的compareAndSwapInt
,该方法是原子操作;
假设有两个线程(t1
、t2
)在竞争锁资源,线程1获取锁资源之后,执行setExclusiveOwnerThread
操作,设置属性值为当前线程t1
此时,当t2
想要获取锁资源,调用lock()
方法之后,执行compareAndSetState(0, 1)
返回false
,会走else
执行acquire()
方法。
方法查看
public final void accquire(int arg) {// tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回falseif (!tryAcquire(arg) &&// 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 线程中断selfInterrupt();
}
accquire()
中涉及的方法比较多,我们将进行拆解,一个一个来分析,顺序:tryAcquire() -> addWaiter() -> acquireQueued()
查看 tryAcquire() 方法
//AQS中
protected boolean tryAcquire(int arg) {//AQS 是基类,具体实现在自己的类中实现,我们去查看“非公平锁”中的实现throw new UnsupportedOperationException();
}//ReentrantLock 中
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();//获取AQS 的 state int c = getState();// 如果 state 为0,代表尝试再次获取锁资源if (c == 0) {// 步骤同上:通过 CAS 的方式尝试将 state 从0改为1,//如果返回 true,代表修改成功,获得锁资源;//如果返回false,代表修改失败,未获取锁资源if (compareAndSetState(0, acquires)) {//设置属性为当前线程setExclusiveOwnerThread(current);return true;}}//当前占有锁资源的线程是否是当前线程,如果是则证明是可重入操作else if (current == getExclusiveOwnerThread()) {//将 state + 1int nextc = c + acquires;//为什么会小于 0 呢?因为最大值 + 1 后会将符号位的0改为1 会变成负数(可参考Integer.MAX_VALUE + 1)if (nextc < 0) // overflow//加1后小于0,超出锁可重入的最大值,抛异常throw new Error("Maximum lock count exceeded");//设置 state 状态setState(nextc);return true;}return false;
}
因为线程1已经获取到了锁,此时state
为1,所以不走nonfairTryAcquire()
的if
。又因为当前是线程2,不是占有当前锁的线程1,所以也不会走else if
,即tryAcquire()
方法返回false
。
查看 addWaiter() 方法
走到本方法中,代表获取锁资源失败。addWaiter()
将没有获取到锁资源的线程甩到队列的尾部。
private Node addWaiter(Node mode) {//创建 Node 类,并且设置 thread 为当前线程,设置为排它锁Node node = new Node(Thread.currentThread(), mode);// 获取 AQS 中队列的尾部节点Node pred = tail;// 如果 tail == null,说明是空队列,// 不为 null,说明现在队列中有数据,if (pred != null) {// 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点node.prev = pred;// CAS 将 tail 节点设置为当前节点if (compareAndSetTail(pred, node)) {// 将之前尾节点的 next 设置为当前节点pred.next = node;// 返回当前节点return node;}}enq(node);return node;
}
当tail
不为空,即队列中有数据时,我们来图解一下pred!=null
代码块中的代码。初始化状态如下,pred
指向尾节点,node
指向新的节点。
node.prev = pred;
将node
的前驱节点设置为pred
指向的节点
compareAndSetTail(pred, node)
通过CAS
的方式尝试将当前节点node
设置为尾结点,此处我们假设设置成功,则FIFO
队列的tail
指向node
节点。
pred.next = node;
将pred
节点的后继节点设置为node
节点,此时node
节点成功进入FIFO
队列尾部。
而当pred
为空,即队列中没有节点或将node
节点设置为尾结点失败时,会走enq()
方法。我们列举的例子就符合pred
为空的情况,就让我们以例子为基础继续分析吧。
//现在没人排队,我是第一个 || 前边CAS失败也会进入这个位置重新往队列尾巴去塞
private Node enq(final Node node) {//死循环for (;;) {//重新获取tail节点Node t = tail;// 没人排队,队列为空if (t == null) {// 初始化一个 Node 为 head,而这个head 没有意义if (compareAndSetHead(new Node()))// 将头尾都指向了这个初始化的Node,第一次循环结束tail = head;} else {// 有人排队,往队列尾巴塞node.prev = t;// CAS 将 tail 节点设置为当前节点if (compareAndSetTail(t, node)) {//将之前尾节点的 next 设置为当前节点t.next = node;return t;}}}
}
进入死循环,首先会走if
方法的逻辑,通过CAS
的方式尝试将一个新节点设置为head
节点,然后将tail
也指向新节点。可以看出队列中的头节点只是个初始化的节点,没有任何意义。
继续走死循环中的代码,此时t
不为null
,所以会走else
方法。将node
的前驱节点指向t
,通过CAS
方式将当前节点node
设置为尾结点,然后将t
的后继节点指向node
。此时线程2的节点就被成功塞入FIFO
队列尾部。
查看 acquireQueued()方法
将已经在队列中的node
尝试去获取锁否则挂起。
final boolean acquireQueued(final Node node, int arg) {// 获取锁资源的标识,失败为 true,成功为 falseboolean failed = true;try {// 线程中断的标识,中断为 true,不中断为 falseboolean interrupted = false;for (;;) {// 获取当前节点的上一个节点final Node p = node.predecessor();//p为头节点,尝试获取锁操作if (p == head && tryAcquire(arg)) {setHead(node);p.next = null;// 将获取锁失败标识置为falsefailed = false;// 获取到锁资源,不会被中断return interrupted;}// p 不是 head 或者 没拿到锁资源,if (shouldParkAfterFailedAcquire(p, node) &&// 基于 Unsafe 的 park方法,挂起线程parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
这里又出现了一次死循环,首先获取当前节点的前驱节点p,如果p是头节点(头节点没有意义),说明node
是head
后的第一个节点,此时当前获取锁资源的线程1可能会释放锁,所以线程2可以再次尝试获取锁。
假设获取成功,证明拿到锁资源了,将node
节点设置为head
节点,并将node
节点的pre
和thread
设置为null
。因为拿到锁资源了,node
节点就不需要排队了。
将头节点p的next
置为null
,此时p节点就不在队列中存在了,可以帮助GC
回收(可达性分析)。failed
设置为false
,表明获取锁成功;interrupted
为false
,则线程不会中断。
如果p不是head
节点或者没有拿到锁资源,会执行下边的代码,因为我们的线程1没有释放锁资源,所以线程2获取锁失败,会继续往下执行。
//该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。int ws = pred.waitStatus;// 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回trueif (ws == Node.SIGNAL)return true;// 如果上一个节点的状态大于0,说明已经失效了if (ws > 0) {do {// 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给 prednode.prev = pred = pred.prev;} while (pred.waitStatus > 0); // 一直找到小于等于0的// 将重新标识好的最近的有效节点的 next 指向当前节点pred.next = node;} else {// 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
只有节点的状态为-1,才会唤醒后一个节点,如果节点状态未设置,默认为0。
图解一下ws>0
的过程,因为ws>0
的节点为失效节点,所以do...while
中会重复向前查找前驱节点,直到找到第一个ws<=0
的节点为止,将node
节点挂到该节点上。
我们的pred
是头结点且未设置状态,所以状态为0,会走else
。通过CAS
尝试将pred
节点的waitStatus
设置为-1,表明node
节点需要被pred
唤醒。
shouldParkAfterFailedAcquire()
返回false
,继续执行acquireQueued()
中的死循环。
步骤和上边一样,node
的前驱节点还是head
,继续尝试获取锁。如果线程1释放了锁,线程2就可以拿到,返回true
;否则继续调用shouldParkAfterFailedAcquire()
,因为上一步已经将前驱结点的ws
设置为-1了,所以直接返回true
。
执行parkAndCheckInterrupt()
方法,通过UNSAFE.park();
方法阻塞当前线程2。等以后执行unpark
方法的时候,如果node
是头节点后的第一个节点,会进入acquireQueued()
方法中走if (p == head && tryAcquire(arg))
的逻辑获取锁资源并结束死循环。
查看cancelAcquire()方法
该方法执行的机率约等于0,为什么这么说呢?因为针对failed
属性,只有JVM
内部出现问题时,才可能出现异常,执行该方法。
// node 为当前节点
private void cancelAcquire(Node node) {if (node == null)return;node.thread = null;// 上一个节点Node pred = node.prev;// 节点状态大于0,说明节点失效while (pred.waitStatus > 0)node.prev = pred = pred.prev;// 将第一个不是失效节点的后继节点声明出来Node predNext = pred.next;// 节点状态变为失效node.waitStatus = Node.CANCELLED;// node为尾节点,cas设置pred为尾节点if (node == tail && compareAndSetTail(node, pred)) {//cas将pred的next设置为nullcompareAndSetNext(pred, predNext, null);} else {int ws;// 中间节点// 如果上一个节点不是head 节点if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||// 前边已经判断了大于0的操作,// pred 是需要唤醒后继节点的,所以当 waitStatus 不为 -1 时,需要将 pred 节点的 waitStatus 设置为 -1 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {Node next = node.next;if (next != null && next.waitStatus <= 0)// CAS 尝试将 pred 的 next 指向当前节点的 nextcompareAndSetNext(pred, predNext, next);} else {// head 节点,唤醒后继节点unparkSuccessor(node);}node.next = node; // help GC}
}
执行到while
时找到前驱节点中最近的有效节点,把当前节点node
挂到有效节点后边,可以过滤掉当前节点前的失效节点。声明出有效节点的第一个后继无效节点predNext
,并把当前的node
节点状态设置为失效状态。
if
中的操作:如果当前节点是尾节点,CAS
尝试将最近的有效节点设置为尾节点,并将尾节点的next
设置为null
。
else
中的操作:
如果pred
节点不是头结点即中间节点,并且pred
的waitStatus
为-1或者waitStatus<=0
,为了让pred
节点能唤醒后继节点,需要设置为-1,并且pred
节点的线程不为空。获取node
节点的后继节点,如果后继节点有效,CAS
尝试将pred
的next
指向node
节点的next
。
当其他节点来找有效节点的时候走当前node
的prev
这条线,而不是再一个一个往前找,可以提高效率。
如果是头结点则唤醒后继节点。
最后将node
节点的next
指向自己。
解锁
释放锁是不区分公平锁和非公平锁的,释放锁的核心是将state
由大于 0 的数置为 0。废话不多说,直接上代码
//释放锁方法
public void unlock() {sync.release(1);
}public final boolean release(int arg) {//尝试释放锁资源,如果释放成功,返回trueif (tryRelease(arg)) {Node h = head;// head 不为空且 head 的 ws 不为0(如果为0,代表后边没有其他线程挂起)if (h != null && h.waitStatus != 0)// AQS的队列中有 node 在排队,并且线程已经挂起// 需要唤醒被挂起的 NodeunparkSuccessor(h);return true;}// 代表释放一次没有完全释放return false;
}
如果释放锁成功,需要获取head
节点。如果头结点不为空且waitStatus
不为0,则证明有node
在排队,执行唤醒挂起其他node
的操作。
查看tryRelease()方法
protected final boolean tryRelease(int releases) {//获取当前锁的状态,先进行减1操作,代表释放一次锁资源int c = getState() - releases;//如果释放锁的线程不是占用锁的线程,直接抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 如果 c 为0 ,代表锁完全释放了,如果不为0,代表锁之前重入了,一次没释放掉,等待下次再次执行时,再次判断if (c == 0) {// 释放锁标志为 true,代表完全释放了free = true;// 将占用互斥锁的标识置为 nullsetExclusiveOwnerThread(null);}// 设置 state 状态setState(c);return free;
}
我们的例子中线程1占用锁资源,线程1释放锁之后,state
为0。进入if
操作,将释放标志更新为true
,将FIFO
队列的exclusiveOwnerThread
标志置为null
。
查看unparkSuccessor()方法
用于唤醒AQS
中被挂起的线程。
// 注意当前的 node 节点是 head 节点
private void unparkSuccessor(Node node) {//获取 head 的状态int ws = node.waitStatus;if (ws < 0)// CAS 将 node 的 ws 设置为0,代表当前 node 接下来会舍弃compareAndSetWaitStatus(node, ws, 0);// 获取头节点的下一个节点Node s = node.next;// 如果下一个节点为null 或者 下一个节点为失效节点,需要找到离 head 最近的有效nodeif (s == null || s.waitStatus > 0) {s = null;// 从尾节点开始往前找不等于null且不是node的节点for (Node t = tail; t != null && t != node; t = t.prev)// 如果该节点有效,则将s节点指向t节点if (t.waitStatus <= 0)s = t;}// 找到最近的node后,直接唤醒if (s != null)LockSupport.unpark(s.thread);
}
问题解析:为什么要从尾结点往前查找呢?
因为在addWaiter
方法中是先给prev
指针赋值,最后才将上一个节点的next
指针赋值,为了避免防止丢失节点或者跳过节点,必须从后往前找。
我们举例中head
节点的状态为-1
,通过CAS
的方式将head
节点的waitStatus
设置为0。
我们的头结点的后继节点是线程2所在的节点,不为null
,所以这边会执行unpark
操作,从上边的acquireQueued()
内的parkAndCheckInterrupt()
方法继续执行。
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);//返回目标线程是否中断的布尔值:中断返回true,不中断返回false,且返回后会重置中断状态为未中断return Thread.interrupted();
}
因为线程2未中断,所以返回false
。继续执行acquireQueued()
中的死循环
for (;;) {// 获取当前节点的上一个节点final Node p = node.predecessor();//p为头节点,尝试获取锁操作if (p == head && tryAcquire(arg)) {setHead(node);p.next = null;// 将获取锁失败标识置为falsefailed = false;// 获取到锁资源,不会被中断return interrupted;}// p 不是 head 或者 没拿到锁资源,if (shouldParkAfterFailedAcquire(p, node) &&// 基于 Unsafe 的 park方法,挂起线程parkAndCheckInterrupt())interrupted = true;
}
此时p是头节点,且能获取锁成功,将exclusiveOwnerThread
设置为线程2,即线程2 获取锁资源。
将node
节点设置为head
节点,并将node
节点的pre
和thread
设置为null
。因为拿到锁资源了,node
节点就不需要排队了。
将头节点p的next
置为null
,此时p节点就不在队列中存在了,可以帮助GC
回收(可达性分析)。failed
设置为false
,表明获取锁成功;interrupted
为false
,则线程不会中断。
为什么被唤醒的线程要调用Thread.interrupted()清除中断标记
从上边的方法可以看出,当parkAndCheckInterrupt()
方法返回true
时,即Thread.interrupted()
方法返回了true
,也就是该线程被中断了。为了让被唤醒的线程继续执行后续获取锁的操作,就需要让中断的线程像没有被中断过一样继续往下执行,所以在返回中断标记的同时要清除中断标记,将其设置为false
。
清除中断标记之后不代表该线程不需要中断了,所以在parkAndCheckInterrupt()
方法返回true
时,要自己设置一个中断标志interrupted = true
,为的就是当获取到锁资源执行完相关的操作之后进行中断补偿,故而需要执行selfInterrupt()
方法中断线程。
以上就是我们加锁解锁的图解过程了。最后我们再来说一下公平锁和非公平锁的区别。
区别
前边已经说过了,似乎非公平锁包含了公平锁的全部操作。打开公平锁的代码,我们发现accquire()
方法中只有该方法的实现有点区别。
hasQueuedPredecessors()
返回false
时才会尝试获取锁资源。该方法代码实现如下
public final boolean hasQueuedPredecessors() {Node t = tail; Node h = head;Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}
h==t
时,队列为空,表示没人排队,可以获取锁资源;- 队列不为空,头结点有后继节点不为空且s节点获取锁的线程是当前线程也可以获取锁资源,代表锁重入操作;
总结
以上就是我们的全部内容了,我们在最后再做个总结:
- 代码使用要合乎规范,避免加锁成功后,在
finally
中无法解锁; - 理解
AQS
的FIFO
队列以及Node
的相关属性,尤其注意waitStatus
的状态; - 利用图加深对非公平锁源码的理解;
跪求一键三连,更文很累的,不要白嫖我,需要一点正反馈。点击名片与我联系,希望在这个冷漠的城市里,让我们互相温暖。
相关文章:

20张图带你彻底了解ReentrantLock加锁解锁的原理
哈喽大家好,我是阿Q。 最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带,发车了。 简单使用 在聊它的源码…...

Dockerfile构建Springboot镜像
Dockerfile构建Springboot镜像 文章目录 Dockerfile构建Springboot镜像 简介实例演示 前期准备 Docker环境Springboot项目Dockerfile文件 Windows 要求构建镜像启动测试 Linux 要求构建镜像启动测试 简介 容器技术大流行的时代,也是docker大流行的时代。 此文…...

从深分页查询到覆盖索引
最近看到一道面试题,如何优化深分页查询 最简单的例子是 select * from web_bill_main limit 30000,10;分页达到30000行,需要把前面29999行都过滤掉,才能找到这10条数据 所以整体时间花了80ms(工具显示时间) 我当时的第一反应是࿰…...

Go语言学习的第三天--下部分(Gin框架的基础了解)
每天都会分享Go的知识,喜欢的朋友关注一下。每天的学习分成两部分基础(必要的,基础不牢地动山摇),另一部分是Go的一些框架知识(会不定时发布,因为小Wei也是一名搬砖人)。但是可以保证…...

JDK的动态代理(powernode 文档)(内含源代码)
JDK的动态代理(powernode 文档)(内含源代码) 源代码下载链接地址:https://download.csdn.net/download/weixin_46411355/87546086 一、动态代理 目录JDK的动态代理(powernode 文档)࿰…...

第1章 多线程基础
第1章 多线程基础 1.1.2 线程与进程的关系 进程可以看成是线程的容器,而线程又可以看成是进程中的执行路径。 1.2 多线程启动 线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。 执行进程中的任务时才会产生线程&a…...

Linux基本指令(一)
文章目录文件操作文档操作系统管理网络通信备份压缩Ctrl Alt T 打开终端 文件操作 1.复制文件 cp afile bfile (将名为afile的文件复制到名为bfile的文件夹中,如果bfile文件不存在,系统将会创建此文件,如果bfile文件已经存在&a…...

el-dialog子组件在mounted周期内获取不到dom?
el-dialog子组件在mounted周期内获取不到dom?一、问题描述二、分析原因三、猜测正常父子组件在mounted生命周期内可以获得dom 父created—子created—子mounted—父mounted----子updated—父updated 一、问题描述 ** el-dialog控制显示隐藏是css控制的display&…...

第九章 opengl之光照(光照贴图)
OpenGL光照贴图漫反射贴图镜面光贴图光照贴图 一个物体的不同部分是不同的材质,那么会有不同的环境光和漫反射颜色表现。 漫反射贴图 原理就是:纹理。 是对同样的原理使用了不同的名字:其实都是使用一张覆盖物体的图像,让我们能…...

JDK动态代理(powernode CD2207 video)(内含教学视频+源代码)
JDK动态代理(powernode CD2207 video)(内含教学视频源代码) 教学视频原代码下载链接地址:https://download.csdn.net/download/weixin_46411355/87545977 目录JDK动态代理(powernode CD2207 video…...

【Linux】Sudo的隐晦bug引发的一次业务问题排查
Sudo的隐晦bug引发的一次业务问题排查写在前面问题描述问题排查高负载现象排查日志排查跟踪任务调度过程Sudo引发的问题手动复现问题分析处理方案写在前面 记录一次生产环境sudo启动进程频繁被Kill且不报错的异常处理过程,如果遇到同样的问题只想要解决方案&#x…...

Java VisualVM 安装 Visual GC 插件图文教程
文章目录1. 通过运行打开 Java VisualVM 监控工具2. 菜单栏初始视图说明3. 工具插件菜单说明4. 手工安装插件5. 重启监控工具查看 Visual GC1. 通过运行打开 Java VisualVM 监控工具 首先确保已安装 Java 环境,如此处安装版本 JDK 1.8.0_161 C:\Users\niaonao>j…...

【C语言】详解静态变量static
关键字static 在C语言中:static是用来修饰变量和函数的static主要作用为:1. 修饰局部变量-静态局部变量 2. 修饰全局变量-静态全局变量3. 修饰函数-静态函数在讲解静态变量之前,我们应该了解静态变量和其他变量的区别: 修饰局部变量 //代码1 #include &l…...

SpringBoot整合ElasticSearch实现模糊查询,排序,分页,高亮
目录 前言 1.框架集成-SpringData-整体介绍 1.1Spring Data Elasticsearch 介绍 2.框架集成Spring Data Elasticsearch 2.1版本说明 2.2.idea创建一个springboot项目 2.3.导入依懒 2.3.增加配置文件 2.4Spring Boot 主程序。 2.5.数据实体类 2.6.配置类 2.7.DAO 数据…...

YARN基本架构
主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成,如图所YA示。 ResourceManager(RM) RM是全局资源管理器,负责整个系统的资源管理和分配 主要由两个组件构成:Scheduler调度器和应用程序…...

【C++复习】类和对象全知识点总结
类和对象写在前面类和对象面向对象类类的定义类的访问限定符类的作用域类的实例化类对象大小this指针类的默认成员函数构造函数析构函数拷贝构造函数运算符重载赋值运算符重载前置后置重载取地址及const取地址操作符重载const 成员static 成员友元友元函数有元类内部类匿名对象…...

基于轻量级YOLOv5开发构建汉字检测识别分析系统
汉字检测、字母检测、手写数字检测、藏文检测、甲骨文检测在我之前的文章中都有做过了,今天主要是因为实际项目的需要,之前的汉字检测模型较为古老了还使用的yolov3时期的模型,检测精度和推理速度都有不小的滞后了,这里要基于yolo…...

leetcode-每日一题-66(简单题,数组)
这道题其实还没那么简单,中间还是有的绕的。。。。给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会…...

LeetCode295之数据流的中位数(相关话题:优先队列)
题目描述 中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。 例如 arr [2,3,4] 的中位数是 3 。例如 arr [2,3] 的中位数是 (2 3) / 2 2.5 。 实现 MedianFinder 类: MedianFinder() 初始化 Media…...

助你加速开发效率!告别IDEA卡顿困扰的性能优化技巧
在现代软件开发中,IDE(集成开发环境)是一个必不可少的工具。IntelliJ IDEA是一个广受欢迎的IDE,但有时候IDE的性能可能会受到影响,导致开发人员的工作效率降低。本文将介绍一些可以提高IDE性能的技巧,帮助开…...

Java设计模式-适配器模式
1、简介 适配器模式是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。 2、适配器模式分类 目标接口(Target&#x…...

Linux 练习六 (IPC 管道)
文章目录1 标准管道流2 无名管道(PIPE)3 命名管道(FIFO)3.1 创建删除管道文件3.2 打开和关闭FIFO文件3.3 管道案例:基于管道的客服端服务器程序使用环境:Ubuntu18.04 使用工具:VMWare workstati…...

合并两个有序链表(精美图示详解哦)
全文目录引言合并两个有序链表题目描述方法一:将第二个链表合并到第一个思路实现方法二:尾插到哨兵位的头节点思路实现总结引言 在前面两篇文章中,我们介绍了几道链表的习题:反转链表、链表的中间结点、链表的倒数第k个结点&…...

33 JSON操作
目录 一、介绍 二、JSON的特点 三、JSON语法 1、json中的数据类型 四、JSON文件的定义 五、读取JSON文件 1、读取json文件的两种方式 (1)read、write (2)json.load 2、使用json.load读取json文件的步骤 3、练习读取json文件 六、练…...

三八妇女节快乐----IT女神活动随笔
献丑了,一首小小散文诗,请大家轻喷 O(≧口≦)O 我的答案 天下芸芸众生,好似夜幕漫天繁星。 与你相识,只是偶然。 简单的一个招呼,于是开始了一段故事。 我们或是诉说,或是分享; 我们彼此倾听&…...

【PSO-PID】使用粒子群算法整定PID参数控制起动机入口压力值
最近在学优化算法,接触到了经典寻优算法之粒子群PSO,然后就想使用PSO算法来调节PID参数,在试验成功之后将此控制算法应用到了空气起动系统上,同时与之前的控制器进行对比看看哪种控制效果最好。 0 引言 PID参数整定主要有两种&…...

当代数据分析指南:激发商业洞见的七个方法(上)
如果说眼下的发生的事能证明什么,那就是基于实时可信的数据分析正在变得越来越重要。但是要是想要在需要的时候准确地获取中肯的洞察,我们所需要的可不只是漂亮的可视化。 如何让你的员工都有能力和机会都做出最好的决策,不管这个决策会有多…...

javaWeb核心02-JSP、EL、JSTL、MVC
文章目录JSP1,JSP 概述2,JSP 快速入门2.1 搭建环境2.2 导入 JSP 依赖2.3 创建 jsp 页面2.4 编写代码2.5 测试3,JSP 原理4,JSP 脚本4.1 JSP 脚本分类4.2 案例4.2.1 需求4.2.2 实现4.2.3 成品代码4.2.4 测试4.3 JSP 缺点5࿰…...

spring-boot+mybatis-plus连接Oracle数据库,及查询相关数据
配置java 略(这里我用的是jdk1.8) 配置maven 环境变量: M2_HOME:D:\LJ\software\java\maven\apache-maven-3.6.3 Path:%M2_HOME%\bin 仓库/jdk/镜像云设置(./config/sitting) 仓库 <localRepository> D:/…...

电商使用CRM系统有什么好处,如何选择
数据显示,使用电商CRM客户管理系统后,企业销售额提高了87%,客户满意度提高了74%,业务效率提高了73%。要在竞争激烈的电商市场取得成功,与目标受众的有效沟通是有效的方法。下面说说什么是电商CRM系统?电商C…...