【Java 并发】AbstractQueuedSynchronizer 中的 Condition

1 简介

任何一个 Java 对象都天然继承于 Object 类, 在线程间实现通信的往往会应用到 Object 的几个方法, 比如 wait(), wait(long timeout), wait(long timeout, int nanos) 与 notify(), notifyAll()
几个方法实现等待 / 通知机制。同样的, 在 Java Lock 体系下也有同样的方法实现等待/通知机制。

从整体上来看 Object 的 wait 和 notify / notify 是与对象监视器配合完成线程间的等待/通知机制, 而 Condition 与 Lock 配合完成等待通知机制,
前者是 Java 底层级别的, 后者是语言级别的, 具有更高的可控制性和扩展性

两者除了在使用方式上不同外, 在功能特性上还是有很多的不同:

  1. Condition 能够支持不响应中断, 而通过使用 Object 方式不支持
  2. Condition 能够支持多个等待队列 (new 多个 Condition 对象), 而 Object 方式只能支持一个
  3. Condition 能够支持超时时间的设置, 而 Object 不支持

参照 Object 的 wait 和 notify / notifyAll 方法, Condition 也提供了同样的方法

2 Condition 实现原理分析

看一下 Condition 的示意图, 方便后续的理解

Alt 'AQS 的 Condition 实现'

2.1 等待队列

创建一个 Condition 对象是通过 lock.newCondition(), 而这个方法实际上是会 new 出一个 ConditionObject 对象, 该类是 AQS 的一个内部类。
Condition 是要和 lock 配合使用的, 也就是 Condition 和 Lock 是绑定在一起的。

我们知道在 Lock 是借助 AQS 实现的, 而 AQS 内部维护了一个同步队列, 如果是独占式锁的话, 所有获取锁失败的线程的会尾插入到同步队列。
同样的, Condition 内部也是使用同样的方式, 内部维护了一个等待队列, 所有调用 Condition.await 方法的线程会加入到等待队列中, 并且线程状态转换为等待状态。

另外注意到 ConditionObject 中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;/** Last node of condition queue. */
private transient Node lastWaiter;

Node 类有这样一个属性:

//后继节点
Node nextWaiter;

进一步说明, 等待队列是一个单向队列。调用 Condition.await 方法后线程依次尾插入到等待队列中。
总的来说 ConditionObject 内部的队列的样子是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同时还有一点需要注意的是: 可以多次调用 lock.newCondition() 方法创建多个 condition 对象, 也就是一个 lock 可以持有多个等待队列。
而在利用 Object 的在实现上只能借助对象 Object 对象监视器上的一个同步队列和一个等待队列, 而并发包中的 Lock 拥有一个同步队列和多个等待队列

如图所示:

MultiConditionInAbstractQueuedSynchronizer
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图所示, ConditionObject 是 AQS 的内部类, 因此每个 ConditionObject 能够访问到 AQS 提供的方法, 相当于每个 Condition 都拥有所属同步器的引用。

3 await 的实现原理

当调用 Condition.await() 方法后会使得当前获取 lock 的线程进入到等待队列, 如果该线程能够从 await() 方法返回的话一定是该线程获取了与 Condition 相关联的 lock 或者被中断

await() 方法源码为:

public final void await() throws InterruptedException {// 当前线程为中断状态if (Thread.interrupted())throw new InterruptedException();// 1. 把当前的线程封装为 Node 节点, 放到队列的尾部, 同时返回这个节点    Node node = addConditionWaiter();// 2. 释放当前线程所占用的 lock, 在释放的过程中会唤醒同步队列中的下一个节点int savedState = fullyRelease(node);int interruptMode = 0;// 判断当前节点是否在 AQS 的同步队列里面while (!isOnSyncQueue(node)) {// 3. 把当前线程挂起, 进入阻塞状态LockSupport.park(this);// 0 不是被中断, 结束循环了if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 4. 自旋等待获取到同步状态, 既 Lock if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null)unlinkCancelledWaiters();// 5. 处理被中断的状态    if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}

大概的流程是这样的:
当前线程调用 Condition.await() 方法后, 会使得当前线程释放 lock, 然后加入到等待队列中, 直至被 signal/signalAll 后会使得当前线程从等待队列中移至到同步队列中去, 再次获得了 lock 后才会从 await 方法返回, 或者在等待时被中断会做中断处理

这里涉及几个问题

  1. 是怎样将当前线程添加到等待队列中去的
  2. 释放锁的过程
  3. 怎样才能从 await 方法退出

3.1 await 中的入队操作 - addConditionWaiter 方法

private Node addConditionWaiter() {// 当前线程是否为持有锁的线程if (!isHeldExclusively())throw new IllegalMonitorStateException();// 取到最后的节点    Node t = lastWaiter;// 尾节点不为空, 同时状态不为 CONDITION (-2)if (t != null && t.waitStatus != Node.CONDITION) {// 把当前链表里面状态值不是 Conditon 的进行删除unlinkCancelledWaiters();// 重试设置 t 为新的尾节点t = lastWaiter;}// 把当前节点封装为 NodeNode node = new Node(Node.CONDITION);// 尾节点为空, 当前节点就是头节点了if (t == null)firstWaiter = node;else// 把当前节点放到队列的尾部t.nextWaiter = node;// 更新当前节点为尾节点    lastWaiter = node;return node;
}private void unlinkCancelledWaiters() {// 头节点Node t = firstWaiter;// 记录最后一个状态是 Condition 的节点, 用于后面赋值给尾节点Node trail = null;// t 节点不为空while (t != null) {// 获取下一个节点Node next = t.nextWaiter;// t 节点的状态不为 CONDITIONif (t.waitStatus != Node.CONDITION) {// 设置 t 的下一个节点为 nullt.nextWaiter = null;// 因为 trail 记录的是遍历中最新的一个状态不是 Condition 的节点// 为 null, 当前一直在移动头节点, 那么只需要把 状态不为 Condition 的 t 节点的下一个节点为头节点即可// 不为 null, trail 表示当前遍历中, 最新的那个状态为 Condition 的节点, 将 t 节点的下一个节点设置到 trail 后面即可if (trail == null)// 设置当前的头节点为 t 节点的下一个节点firstWaiter = next;else// trail 的下一个节点等于 t 节点的下一个节点trail.nextWaiter = next;// 没有下一个节点了if (next == null)// 将尾节设置为 traillastWaiter = trail;} else// 设置 trail = ttrail = t;// t = t 的下一个节点    t = next;}
}

3.2 await 中的锁释放操作 - fullyRelease 方法

将当前节点插入到等待对列之后, 会使当前线程释放 lock, 由 fullyRelease 方法实现, fullyRelease 源码为:

final int fullyRelease(Node node) {try {// 获取同步状态int savedState = getState();// 调用 AQS 的方法释放锁if (release(savedState))// 释放成功return savedState;// 释放失败, 抛出异常    throw new IllegalMonitorStateException();} catch (Throwable t) {// 释放锁的节点的状态修改为 Cancelled 取消状态node.waitStatus = Node.CANCELLED;throw t;}
}

调用 AQS 的模板方法 release 方法释放 AQS 的同步状态并且唤醒在同步队列中头节点的后继节点引用的线程。

如果释放成功则正常返回, 若失败的话就抛出异常。这样就解决了上面的第 2 个问题了。

3.3 await 中的判断当前节点是否在等待队列的操作 - isOnSyncQueue 方法

final boolean isOnSyncQueue(Node node) {// 当前节点的状态为 condition 或没有上一个节点, 也就是头节点了, 直接返回 falseif (node.waitStatus == Node.CONDITION || node.prev == null)return false;// 没有下一个节点, 也就是理论理论上的头节点, 直接返回 trueif (node.next != null) return true;// 从链表的尾节点开始向上找, 是否有等于这个节点return findNodeFromTail(node);
}private boolean findNodeFromTail(Node node) {// 从尾节点一直往前找for (Node p = tail;;) {// 当前节点和 node 一样 返回 trueif (p == node)return true;// p 节点为 null, 没有数据了, 返回 false    if (p == null)return false;p = p.prev;}
}

整理一下, 逻辑如下

  1. 当前节点为 condition 状态或者没有上一节点, 也就是头节点, 直接返回 false
  2. 当前节点没有下一个节点了, 也就是理论上的头节点, 直接返回 true
  3. 从链表的尾节点开始往前找是否有和判断的节点一样的, 有返回 ture, 没有 false

3.4 await 中的线程线程唤醒中断判断 - checkInterruptWhileWaiting 方法

这里简单说一下 Condition.signal() 方法的原理

  1. 找到当前的等待队列的头节点
  2. 通过 CAS 将头节点的状态从 condition 设置为 0 (加入等待队列的线程的状态默认为 condition)
  3. 第二步的 CAS 失败了, 会重新从等待队列中找下一个节点, 然后从第二步继续开始
  4. 第二步的 CAS 成功了, 把当前的节点放到同步队列的尾部, 同时返回上一次的尾节点
  5. 上一次的尾节点的状态为取消状态, 或者通过 CAS 将上一次的尾节点的状态设置为 signal 状态失败的话, 调用 LockSupport.unpark(上一次尾节点里面的线程), 对齐进行唤醒

上面大体就是 signal 的步骤, 理解完上面的步骤, 才能理解下面的代码, 重点, 重点。

在上面 await 方法的源码中, 知道线程是通过 LockSupport.park(this) 挂起的。 那么什么时候这个阻塞的方法会继续执行, 也就是对应的线程苏醒。

  1. 通过调用 LockSupport.unpark(当前线程), 唤醒当前线程
  2. 别的线程调用了当前线程的 interrupt 方法, 中断当前线程, 这是 LockSupport.park(this), 也会被唤醒, 不会抛中断异常的
  3. 线程假唤醒

所以线程在 LockSupport.park(this) 处苏醒, 继续走下去的, 可能性有 3 种, 这时候需要先判断当前线程是否为中断导致的属性


private int checkInterruptWhileWaiting(Node node) {// 判断当前线程在等待期间, 是否被设置了中断标识, 这个方法会返回 3 种情况// 0: 正常情况, 没有设置中断标识, 表示是正常的同步队列唤醒或者外部直接通过 LockSupport.unpark() 唤醒这个线程// THROW_IE (-1): 线程唤醒后, 抛出异常// REINTERRUPT (1): 线程唤醒后, 设置中断标识为 true// THROW_IE 和 REINTERRUPT 都是表示线程在等待中被设置了中断标识, 他们是如何区别的?// 需要分析 transferAfterCancelledWait 方法中的, 将当前节点的状态通过 CAS 从 condition 设置为 0 是否成功// 那么什么时候是成功, 什么时候会失败, 这时就涉及到上面说的 signal()/signalAll() 方法的流程了// 1. 直接调用了这个线程的 interrupt() 方法, 这时候节点的状态还是 condition 的, 成功// 2. 一个线程调用了 signal()/ signalAll() 方法, 这时候执行到了上面步骤的第二步, 并成功了, 这时候另一个线程直接中断了线程, 这时候节点的状态不是 condition 的, 失败了// 3. 一个线程调用了 signal()/ signalAll() 方法, 这时候还未执行到上面步骤的第二步, 这时候另一个线程直接中断了线程, 这时候节点的状态还是 condition 的, 成功// 上面 3 种情况, 可以简单的概况为 // 1. 调用了 signal()/ signalAll() 后, 中断线程// 2. 中断了线程后, 调用了 signal()/ signalAll()// 如果是第一种情况, 则由 signal() / signalALl() 将中断线程的节点放到同步队列的尾部// 如果是第二种情况, 则由 transferAfterCancelledWait() 将中断线程的节点放到同步队列的尾部// 在第一种情况下, 存在将中断线程的节点从等待队列移动到同步队列的过程, // 这个过程会出现一个很端的时间, 在同步队列找不到这个节点, 所以在后面做多了一层判断, 直到在同步队列中找到了线程的节点, 才返回// 2 种不同的中断方式// 第一种返回了 REINTERRUPT, 线程唤醒后, 设置中断标识为 true// 第二种返回了 THROW_IE, 线程唤醒后, 抛出异常return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :  0;
}final boolean transferAfterCancelledWait(Node node) {// 把 node 节点的状态从 condition 设置为 0,// 设置成功, 把节点放到同步队列的尾部, 返回 true// 在 signal/signalAll 中会先将节点设置为 signal 状态, 即 0, 然后把节点从等待队列移动到同步队列// 在这里将节点的状态从 condition 设置为 0, 设置成功了, 表示在 signal/signalAll 之前, 线程就被中断了// 设置失败, 在表示在 signal/signalAll 之后, 线程才被中断if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {enq(node);return true;}// 将中断线程的节点从等待队列移动到同步队列的过程, 存在极端时间的节点找不到while (!isOnSyncQueue(node))Thread.yield();return false;
}

3.5 await 的退出流程

public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();Node node = addConditionWaiter();int savedState = fullyRelease(node);int interruptMode = 0;// 当前节点是否在等待队列中while (!isOnSyncQueue(node)) {// 不在的话, 挂起当前线程LockSupport.park(this);// 唤醒后判断当前线程的是否被中断过// 等于 0, 表示没有被中断// 不等于 0, 表示中断过, 存在 2 种中断方式if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// acquireQueued 的返回值, true 线程需要中断, false 线程不需要中断// acquireQueued 返回需要中断, 则将不是 THROW_IE 的中断模式设置为 REINTERRUPTif (acquireQueued(node, savedState) && interruptMode != THROW_IE)// 也就是将 0 的情况也设置为 REINTERRUPTinterruptMode = REINTERRUPT;// 节点的下一个节点不为 null, 移除当前链表中的取消状态的节点    if (node.nextWaiter != null)unlinkCancelledWaiters();// 中断模式不为 0, 根据中断模式设置线程的中断状态   if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {// 中断模式为 THROW_IE, 抛出异常if (interruptMode == THROW_IE)throw new InterruptedException();// 中断模式为 REINTERRUPT, 设置线程的中断标识为 true    else if (interruptMode == REINTERRUPT)selfInterrupt();
}

上面就是 await() 方法的整体流程梳理。

上面的第 3 个问题: 怎样才能从 await 方法退出 也就能得到答案了

  1. 当前线程被中断, 从等待队列移动到同步队列后, 重新获取锁
  2. 调用 signal/signalAll 将当前线程的节点从等待队列移到同步队列, 重新获取锁

3.6 await 类似的其他挂起线程的方法

3.6.1 超时机制的支持

condition 还额外支持了超时机制, 使用者可调用方法 awaitNanos(), awaitUtil() 这两个方法的实现原理, 基本上与 AQS 中的 tryAcquire 方法如出一辙。

2 者的实现方式和 await 类似, 只是在死循环中添加了对超时时间的判断。同时在超时后, 将当前线程的节点的状态从 condition 设置为 0, 追加到同步队列的尾部

3.6.2 不响应中断的支持

要想不响应中断可以调用 condition.awaitUninterruptibly() 方法, 该方法的源码为:

public final void awaitUninterruptibly() {// 节点入到等待队列Node node = addConditionWaiter();// 释放锁int savedState = fullyRelease(node);// 是否需要设置中断标识boolean interrupted = false;// 判断当前节点不在同步队列中, 在的话, 结束循环while (!isOnSyncQueue(node)) {// 挂起线程LockSupport.park(this);// 线程苏醒后, 判断中断标识是否为 trueif (Thread.interrupted())interrupted = true;}// 从同步队列中获取锁成功了, 同时返回需要设置中断 或者上面的判断后需要中断if (acquireQueued(node, savedState) || interrupted)// 设置线程的中断标识为 trueselfInterrupt();}

这段方法与上面的 await 方法基本一致, 只不过减少了对中断的处理, 并省略了 reportInterruptAfterWait 方法抛被中断的异常。

4 signal/signalAll 实现原理

调用 condition 的 signal 或者 signalAll 方法可以将等待队列中等待时间最长的节点移动到同步队列中, 使得该节点能够有机会获得 lock。
按照等待队列是先进先出 (FIFO) 的, 所以等待队列的头节点必然会是等待时间最长的节点, 也就是每次调用 condition 的 signal 方法是将头节点移动到同步队列中。

signal 方法源码为:

public final void signal() {// 检测当前线程是否持有锁if (!isHeldExclusively())// 没有锁, 抛出异常throw new IllegalMonitorStateException();// 获取头节点    Node first = firstWaiter;if (first != null)// 尝试将头节点移动到同步队列doSignal(first);
}private void doSignal(Node first) {do {// 设置头节点为当前头节点的下一个节点if ((firstWaiter = first.nextWaiter) == null)// 当前的头节点为空, 设置尾节点也为空lastWaiter = null;// 置空需要移动的节点的下一个节点    first.nextWaiter = null;// transferForSignal 进行节点的真正处理// 在 transferForSignal 处理失败时, 最新的头节点不为空, 继续处理新的头节点} while (!transferForSignal(first) &&  (first = firstWaiter) != null);
}final boolean transferForSignal(Node node) {// 1. 将节点的状态从 condition 设置为 0if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))return false;// 2. 把这个节点放到 AQS 里面的同步队列的尾部, 同时获取到上一次的尾节点Node p = enq(node);int ws = p.waitStatus;// 上一次的尾节点状态为 1 (取消状态) 或者 通过 CAS 将这个节点的状态设置为 signal 失败, 则唤醒这个线程if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))LockSupport.unpark(node.thread);return true;
}

signal方法的逻辑:

  1. 将头节点的状态更改为CONDITION
  2. 调用 enq 方法, 将该节点尾插入到同步队列中

现在我们可以得到结论: 调用 condition 的 signal 的前提条件是当前线程已经获取了 lock, 该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列, 而移入到同步队列后才有机会使得等待线程被唤醒,
即从 await 方法中的 LockSupport.park(this) 方法中返回, 从而才有机会使得调用 await 方法的线程成功退出。

signalAll

sigllAll 与 sigal 方法的区别体现在 doSignalAll 方法上, 前面我们已经知道 doSignal 方法只会对等待队列的头节点进行操作, 而 doSignalAll 的源码:

private void doSignalAll(Node first) {// 将头尾节点都设置为 nulllastWaiter = firstWaiter = null;do {// 获取头节点的下一个节点Node next = first.nextWaiter;// 设置投节点的下一个节点为 nullfirst.nextWaiter = null;// 尝试将头节点设置到同步队列的尾部transferForSignal(first);// 头节点等于头节点的下一个节点first = next;} while (first != null);
}

该方法会不断地将等待队列中的每一个节点都移入到同步队列中, 即 “通知” 当前调用 condition.await() 方法的每一个线程。

5 await 与 signal-signalAll 的结合思考

文章开篇提到等待/通知机制, 通过使用 condition 提供的 await 和 signal/signalAll 方法就可以实现这种机制, 而这种机制能够解决最经典的问题就是 “生产者与消费者问题”。
await 和 signal 和 signalAll 方法就像一个开关控制着线程 A (等待方) 和线程 B (通知方) 。它们之间的关系可以用下面一个图来表现得更加贴切:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图:

  1. 线程 awaitThread 先通过 lock.lock() 方法获取锁成功后调用了 condition.await 方法进入等待队列
  2. 另一个线程 signalThread 通过 lock.lock() 方法获取锁成功后调用了condition.signal 或者 signalAll 方法, 使得线程 awaitThread 能够有机会移入到同步队列中,
  3. 当其他线程释放 lock 后使得线程 awaitThread 能够有机会获取 lock, 从而使得线程 awaitThread 能够从 await 方法中退出执行后续操作。如果 awaitThread 获取 lock 失败会直接进入到同步队列

举个例子

public class AwaitSignal {private static ReentrantLock lock = new ReentrantLock();private static Condition condition = lock.newCondition();private static volatile boolean flag = false;public static void main(String[] args) {Thread waiter = new Thread(new Waiter());waiter.start();Thread signaler = new Thread(new Signaler());signaler.start();}static class Waiter implements Runnable {@Overridepublic void run() {lock.lock();try {while (!flag) {System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");} finally {lock.unlock();}}}static class Signaler implements Runnable {@Overridepublic void run() {lock.lock();try {flag = true;condition.signalAll();} finally {lock.unlock();}}}
}

输出结果

Thread-0当前条件不满足等待  
Thread-0接收到通知, 条件满足

6 参考

详解Condition的await和signal等待/通知机制
Condition的await-signal流程详解

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

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

相关文章

工具篇--分布式定时任务springBoot--elasticjob简单使用(1)

文章目录 前言一、elasticjob 介绍:二、elasticjob 使用:2.1 部署zookeeper:2.2 引入库2.2 定义任务:2.3 任务执行:2.4 任务执行控制台输出: 三、elasticjob 启动错误:3.1 KeeperErrorCode Ope…

【Node.js从基础到高级运用】十二、身份验证与授权:JWT

身份验证与授权是现代Web应用中不可或缺的部分。了解如何在Node.js应用中实施这些机制,将使你能够构建更安全、更可靠的应用程序。本文将引导你通过使用JWT实现用户注册、登录和权限控制的过程。 JWT(Json Web Token) JWT是一种用于双方之间…

蓝桥杯深度优先搜索|剪枝|N皇后问题|路径之谜(C++)

搜索:暴力法算法思想的具体实现 搜索:通用的方法,一个问题如果比较难,那么先尝试一下搜索,或许能启发出更好的算法 技巧:竞赛时遇到不会的难题,用搜索提交一下,说不定部分判题数据很…

R语言tidycmprsk包分析竞争风险模型

竞争风险模型就是指在临床事件中出现和它竞争的结局事件,这是事件会导致原有结局的改变,因此叫做竞争风险模型。比如我们想观察患者肿瘤的复发情况,但是患者在观察期突然车祸死亡,或者因其他疾病死亡,这样我们就观察不…

基于单片机的太阳能热水器控制系统设计与仿真

目录 摘要 3 Controling system design and simulation of the solar water heater based on single chip microcomputer 4 第一章 前言 5 1.1设计背景和意义 5 1.2国内外的发展趋势 5 第二章 系统设计总览 7 2.1控制中心 7 2.2外围设备 7 第三章 系统硬件设计 8 3.1 总硬件的…

小程序路由跳转---事件通信通道EventChannel(二)

事件通信通道EventChannel实现两个页面之间的数据传输已在上篇小程序路由跳转—事件通信通道EventChannel(一)展开叙述,接下来讨论下多个页面(三个及以上)数据的通道如何构建。 本文重点:三个以上页面需将…

jenkin部署spring boot项目【从0到1】

写在前面,遇到的很多错误 本来想用docker启动Jenkins的,也这样做了,但是遇到了一个非常严重的问题,就是mvn命令在docker里面不生效,然后就修改文件,但是发现vi不存在,好的。接着用yum安装vi工具…

Vue2 引入自己下载的SVG图像的方式

Vue2 引入下载的SVG图像的方式 Step 1:安装依赖 npm i svg-sprite-loader --saveStep 2:创建文件路径 // index.js import Vue from vue import SvgIcon from /components/SvgIcon// svg component// register globally Vue.component(svg-icon, Svg…

《JAVA与模式》之抽象工厂模式

系列文章目录 文章目录 系列文章目录前言一、使用简单工厂模式的解决方案二、引进抽象工厂模式三、抽象工厂模式结构四、抽象工厂模式的优缺点前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看…

Hubspot 2023年推荐使用的11个AI视频生成器

视频是任何营销活动不可或缺的一部分;然而,如果你不懂编辑或时间紧迫,它们可能会很乏味,很难创建。一只手从电脑里伸出来,拳头碰到另一只手;代表AI视频生成器。 幸运的是,你可以利用许多人工智能…

【数据分析】数据分析介绍

专栏文章索引:【数据分析】专栏文章索引 目录 一、介绍 二、生活中的数据分析 1.无处不在的数据 2.为什么要进行数据分析? 三、数据挖掘案例 1.案例分析 一、介绍 数据采集:数据采集是指从不同来源收集原始数据的过程,包括…

考研失败, 学点Java打小工——Day3

1 编码规范——卫语句 表达异常分支时&#xff0c;少用if-else方式。   比如成绩判断中对于非法输入的处理&#xff1a; /*>90 <100 优秀>80 <90 良好>70 <80 一般>60 <70 及格<60 不及格*/Testpu…

Twitter广告投放技巧

明确目标受众&#xff1a; 确定你的目标受众&#xff0c;包括他们的兴趣、地理位置、年龄等。Twitter提供了广告定位选项&#xff0c;确保你的广告被展示给最相关的用户。 使用吸引人的图像和视频&#xff1a; 在Twitter上&#xff0c;图像和视频是引起关注的关键。确保你的广…

小程序开发平台源码系统:万能建站门店小程序功能 带完整的搭建教程以及代码包

在移动互联网时代&#xff0c;小程序以其独特的优势&#xff0c;迅速占领了市场的一席之地。然而&#xff0c;对于许多中小企业和个人开发者来说&#xff0c;缺乏专业的开发团队和技术支持&#xff0c;使得小程序开发成为一项难以逾越的技术门槛。小编给大家分享一款万能建站门…

linux 使用docker安装 postgres 教程,踩坑实践

linux 使用docker安装 postgres 教程 踩坑实践,安装好了不能远程访问。 防火墙已关闭、postgres 配置了允许所有ip 访问、网络是通的。端口也是开放的&#xff0c;就是不能用数据库链接工具访问。 最后发现是云服务器端口没开 ,将其打开 到这一步完全正确了&#xff0c;但是…

Python AI 之Stable-Diffusion-WebUI

Stable-Diffusion-WebUI简介 通过Gradio库&#xff0c;实现Stable Diffusion web 管理接口 Windows 11 安装Stable-Diffusion-WebUI 个人认为Stable-Diffusion-WebUI 官网提供的代码安装手册/自动安装不适合新手安装&#xff0c;我这边将一步步讲述我是如何搭建Python Conda…

【数据结构】单向链表的创建及4种应用

目录 前言 自定义“单向”链表类 1. 自定义一个链表类&#xff0c;并完成“初始化链表”、“添加元素&#xff08;头插法/尾插法&#xff09;”、“计算链表长度”操作&#xff1b; 自定义链表 向链表中插入元素&#xff08;头插法&#xff09; 向链表中插入元素&#xf…

2024年AI辅助研发:技术革新引领研发新纪元

文章目录 &#x1f4d1;前言一、AI辅助研发的技术进展二、行业应用案例三、面临的挑战与机遇四、未来趋势预测全篇总结 &#x1f4d1;前言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已逐渐成为推动社会进步的重要力量。特别是在研发领域&#xff0c;A…

计算机视觉——目标检测(R-CNN、Fast R-CNN、Faster R-CNN )

前言、相关知识 1.闭集和开集 开集&#xff1a;识别训练集不存在的样本类别。闭集&#xff1a;识别训练集已知的样本类别。 2.多模态信息融合 文本和图像&#xff0c;文本的语义信息映射成词向量&#xff0c;形成词典&#xff0c;嵌入到n维空间。 图片内容信息提取特征&…

【北京大学】徐高《金融经济学二十五讲》

一、经济的任务 经济的任务之一是确保有效地分配稀缺资源&#xff0c;这是经济学中的一个核心问题。资源是有限的&#xff0c;而需求是无限的&#xff0c;因此经济系统需要通过合理的机制来分配资源以满足社会的需求。以下是关于经济分配资源的几个方面&#xff1a; 1. 资源配…