重磅出击,20张图带你彻底了解ReentrantLock加锁解锁的原理

最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带,发车了。

简单使用

在聊它的源码之前,我们先来做个简单的使用说明。当我在IDEA中创建了一个简单的Demo之后,它会给出以下提示

提示文字

在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

  • 1、如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。

  • 2、如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQStryRelease方法(取决于具体实现类),抛出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,如果其他线程调用了Conditionsignal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;

  • 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():底层调用的是unsafecompareAndSwapInt,该方法是原子操作;

假设有两个线程(t1t2)在竞争锁资源,线程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是头节点(头节点没有意义),说明nodehead后的第一个节点,此时当前获取锁资源的线程1可能会释放锁,所以线程2可以再次尝试获取锁。

假设获取成功,证明拿到锁资源了,将node节点设置为head节点,并将node节点的prethread设置为null。因为拿到锁资源了,node节点就不需要排队了。

将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interruptedfalse,则线程不会中断。

如果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节点不是头结点即中间节点,并且predwaitStatus为-1或者waitStatus<=0,为了让pred节点能唤醒后继节点,需要设置为-1,并且pred节点的线程不为空。获取node节点的后继节点,如果后继节点有效,CAS尝试将prednext指向node节点的next

当其他节点来找有效节点的时候走当前nodeprev这条线,而不是再一个一个往前找,可以提高效率。

如果是头结点则唤醒后继节点。

最后将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节点的prethread设置为null。因为拿到锁资源了,node节点就不需要排队了。

将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interruptedfalse,则线程不会中断。

为什么被唤醒的线程要调用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中无法解锁;

  • 理解AQSFIFO队列以及Node的相关属性,尤其注意waitStatus的状态;

  • 利用图加深对非公平锁源码的理解;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/36549.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入源码谈谈ReentrantLock中的公平锁和非公平锁的加锁机制

前言 ReentrantLock和synchronized一样都是实现线程同步&#xff0c;但是像比synchronized它更加灵活、强大、增加了轮询、超时、中断等高级功能&#xff0c;可以更加精细化的控制线程同步&#xff0c;它是基于AQS实现的锁&#xff0c;他支持公平锁和非公平锁&#xff0c;同时…

【图解】一篇搞定ReentrantLock的加锁和解锁过程

文章目录 1. 概述2. AbstractQueuedSynchronizer&#xff08;AQS&#xff09;3. 加锁4. 解锁5. 公平锁和非公平锁的区别 1. 概述 本文主要结合图片分析ReentrantLock加锁和解锁过程的源码&#xff0c;加锁和解锁的原理不清楚的读者可以好好看看。 2. AbstractQueuedSynchroni…

Lock锁和ReentrantLock锁

前言 JDK 1.5中提供的锁的接口java.util.concurrent.locks.Lock&#xff0c;其提供了一些ReentrantLock, ReentrantReadWriteLock实现类。 参考JDK文档&#xff1a;Java Platform SE 6 目录 前言 Lock接口 ReentrantLock 公平性和非公平性 公平锁与非公平锁的使用示例 A…

ReentrantLock源码分析(一)加锁流程分析

一、ReetrantLock的使用示例 static ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws InterruptedException { new Thread(ClassLayOutTest::reentrantLockDemo, "threadA").start(); Thread.sleep(1000);…

Lock与ReentrantLock

Lock Lock位于java.util.concurrent.locks包下&#xff0c;是一种线程同步机制&#xff0c;就像synchronized块一样。但是&#xff0c;Lock比synchronized块更灵活、更复杂。 1. Lock继承关系 2. 官方文档解读 3. Lock接口方法解析 public interface Lock {// 获取锁。如果锁…

ReentrantLock介绍

文章目录 ReentrantLock1、构造函数2、公平性锁和非公平性锁&#xff08;1&#xff09;公平性锁和非公平性锁示例&#xff08;2&#xff09;公平锁和非公平锁的实现公平性锁&#xff1a;FairLock非公平性锁 &#xff08;3&#xff09;Condition生产者和消费者 循环打印ABC Reen…

ReentrantLock锁相关方法

目录 Lock接口的实现类 ReentrantLock的方法 ReentrantLockTest测试 用于测试的线程 t1测试 正确释放重入锁 获取当前的重入次数 t1t2测试 使用islocked()方法检测锁状态 t1t3测试 使用trylock方法尝试获取锁 使用isHeldByCurrentThread方法检测当前线程是否持有锁 不…

ReentrantLock详解

目录 一、ReentrantLock的含义 二、RerntrantLock当中的常用方法 ①lock()和unlock()方法 ②构造方法 ③tryLock()方法 tryLock()无参数 tryLock(timeout,Times)有参数 ④lockInterruptibly() throws InterruotedException 经典面试问题: ReentrantLock和synchronized有什…

OpenAI最新官方ChatGPT聊天插件接口《智能聊天插件引言》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(一)(附源码)

Chat Plugins Limited Alpha 聊天插件 前言IntroductionPlugin flow 插件流其它资料下载 Learn how to build a plugin that allows ChatGPT to intelligently call your API. 了解如何构建允许ChatGPT智能调用API的插件。 前言 在现代的软件开发环境中&#xff0c;使用第三方…

Pycharm快速入门(10) — 插件管理

1、插件安装 File | Settings | Plugins | Marketplace 搜索插件点击Install安装 2、插件卸载 File | Settings | Plugins | Installed 选择需要卸载的插件&#xff0c;点击Uninstall。 3、推荐插件 &#xff08;1&#xff09;、Chinese ​(Simplified)​ Language Pack &am…

chatgpt赋能python:Python编程的好玩之处:用简单的代码创造奇妙的世界

Python编程的好玩之处&#xff1a;用简单的代码创造奇妙的世界 如果你喜欢写代码&#xff0c;那么Python是一个不错的选择。Python语言设计简单&#xff0c;易学易用&#xff0c;同时还拥有丰富的生态系统&#xff0c;支持许多强大的第三方库和框架&#xff0c;可以使你轻松地…

chatgpt赋能python:Python图片拼图的好处和应用

Python图片拼图的好处和应用 Python是一种高级编程语言&#xff0c;已经被广泛应用于数据科学、网络编程、机器学习等领域。其中&#xff0c;Python的图像处理领域也越来越受关注。在本文中&#xff0c;我们将介绍如何使用Python创建图片拼图&#xff0c;并讨论它的好处和应用…

midjourney教程:如何快速生成个性化Logo设计

midjourney是一款基于人工智能技术的Logo设计工具&#xff0c;它可以帮助用户快速生成个性化的Logo设计&#xff0c;而无需具备专业的设计技能。下面将为大家介绍midjourney的使用方法&#xff0c;以帮助大家轻松生成符合自己需求的Logo设计。 第一步&#xff1a;登录midjourn…

chatgpt赋能python:Python添加图片背景的方法

Python添加图片背景的方法 简介 Python是一种开源的高级编程语言&#xff0c;广泛应用于各个行业中&#xff0c;包括图像处理。添加图片背景是图像处理中的常见需求&#xff0c;通过Python可以很方便地实现。 本篇文章将介绍如何使用Python来给图片添加背景&#xff0c;让您…

chatgpt赋能python:Python怎么做图形

Python怎么做图形 在数据可视化和图像处理方面&#xff0c;Python已经成为了最受欢迎的编程语言之一。Python的图形库使得创建各种图形和图表、可视化工具和图像处理应用程序变得容易而简单。 在本文中&#xff0c;我们将会介绍一些最受欢迎的Python图形库&#xff0c;以帮助…

程序员晒追女神聊天截图,坦言第一次没经验,网友直呼凭实力单身

前段时间网络上一名程序员晒出了自己与女神之间的聊天记录的对话截图&#xff0c;通过截图中我们可以看出&#xff0c;应该是这位程序员在追求这位女神&#xff0c;但是短短的十几分钟几条聊天记录&#xff0c;却以女神不再愿意搭理程序员结束&#xff0c;对于这样的结局&#…

程序员给女友4千生活费,收到女友错发信息后分手,神对话!

如何平衡好亲情爱情的关系&#xff0c;是一门学问&#xff0c;有的人就希望自己的另一半过好他们自己的小日子&#xff0c;不要对家里的事情过多的付出&#xff0c;但有人觉得自己父母养大自己不容易&#xff0c;能有能力的话&#xff0c;不光孝敬爹妈&#xff0c;还会帮衬家里…

程序员就是这样聊天把女朋友聊没的

身为程序员 都想当然的认为 身为一个优秀的程序员 我怎么可能会没女票 这不科学啊 工资高&#xff0c;话少 有一天看到了 某个程序员的聊天记录 有女孩主动搭讪 这么绝好的机会 然后你竟然说忙 说忙 忙... 主动找你搭讪 你还不抓紧机会约约约 如果改成&#xff1a…

被程序员的相亲规划整不会了......

近日&#xff0c;北京一程序员将自己7天7场相亲行程规划表发到论坛分享&#xff0c;感叹到&#xff1a;真不是凡尔赛&#xff0c;相亲比上班还难&#xff0c;引来大量网友围观。 相亲也有规划表&#xff1f; 据介绍&#xff0c;该程序员今年刚好30岁&#xff0c;自己平时加班多…

程序员吐槽女朋友狮子大开口

本文转载自程序员八卦 一个程序员发帖吐槽自己的潮汕女朋友&#xff0c;开口要彩礼18万8&#xff0c;楼主在网上查了一下&#xff0c;一般潮汕彩礼是3万到8万&#xff0c;难道外地人要多给一点吗&#xff1f;而且女朋友还一定要楼主父母出彩礼&#xff0c;不能楼主自己出&…