Java JUC(三) AQS与同步工具详解
Java JUC(三) AQS与同步工具详解
一. ReentrantLock 概述
ReentrantLock
是 java.util.concurrent.locks
包下的一个同步工具类,它实现了 Lock
接口,提供了一种相比synchronized
关键字更灵活的锁机制。ReentrantLock
是一种独占式且可重入的锁,并且支持可中断、公平锁/非公平锁、超时等待、条件变量等高级特性。其特点如下:
- 独占式: 一把锁在同一时间只能被一个线程所获取;
- 可重入: 可重入意味着同一个线程如果已持有某个锁,则可以继续多次获得该锁(注意释放同样次之后才算完全释放成功);
- 可中断: 在线程获取锁的等待过程中可以中断获取,放弃等待而转去执行其他逻辑;
- 公平性:
ReentrantLock
支持公平锁和非公平锁(默认)两种模式。其中,公平锁会按照线程请求锁的顺序来分配锁(降低性能),而非公平锁允许线程抢占已等待的线程的锁(可能存在饥饿现象); - 条件变量: 通过
Condition
接口的实现,允许线程在某些条件下等待或唤醒,即可以实现选择性通知;
Type | Method | Description |
---|---|---|
/ | ReentrantLock() | 无参构造方法,默认为非公平锁 |
/ | ReentrantLock(boolean fair) | 带参构造方法,其中fair 表示锁的公平性策略:- true : 公平锁- false : 非公平锁 |
void | lock() | 不可中断式获取锁。若当前锁已被其他线程持有,则阻塞等待;**注意:**该获锁过程不可被中断 |
void | lockInterruptibly() throws InterruptedException | 可中断式获取锁。若当前锁已被其他线程持有,则阻塞等待;**注意:**该获锁等待过程可被中断,抛出InterruptedException ,并清除当前线程的中断状态 |
boolean | tryLock() | 尝试获取锁,该方法会立即返回。若获锁成功,则返回true ,否则将返回false ;**注意:**该方法会破坏公平锁配置,即在公平锁策略下,该方法也会立即尝试获取可用锁 |
boolean | tryLock(long timeout,TimeUnit unit) throws InterruptedException | 在给定时间timeout 内尝试获取锁。若获锁成功,则返回true ,否则将阻塞等待直到timeout 过期,返回false ;注意: - 获锁等待过程可被中断,抛出 InterruptedException ,并清除当前线程的中断状态- 遵循公平锁配置策略,即在公平锁策略下,该方法会按顺序等待获取锁 |
void | unlock() | 当前线程尝试释放该锁。若当前线程未持有该锁,则抛出IllegalMonitorStateException 异常 |
Condition | newCondition() | 返回一个与当前Lock 实例绑定的条件变量集合对象(默认返回AQS 内部实现类ConditionObject ),用于实现线程的条件等待/唤醒(详见后文) |
1. 锁的基本使用
相比synchronized
关键字来说,ReentrantLock
属于显式锁,其锁机制都是针对Lock
实例对象本身进行加锁,并且在使用过程中需要手动释放,即锁的获取与释放是成对出现的;除此之外,ReentrantLock
属于JDK API层面实现的互斥锁,其通过方法调用实现锁功能,可以跨方法从而更加灵活。为了避免出现死锁问题,官方建议的开发方式如下:
// new lock object
ReentrantLock lock = new ReentrantLock();
lock.lock(); // block until condition holds
try {// ... method body
} finally {lock.unlock(); // release
}
问题背景: 假设当前有一个卖票系统,一共有100张票,有4个窗口同时售卖,请模拟该卖票过程,注意保证出票的正确性。
public class Test_01 {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();for (int i = 0; i < 4; i++){new Thread(new TicketSeller(lock), "Thread_" + i).start();}}
}
// 售票类实现
class TicketSeller implements Runnable{private static int ticketCount = 100;private ReentrantLock windowLock;public TicketSeller(ReentrantLock lock) {this.windowLock = lock;}@Overridepublic void run() {while (true) {try {Thread.sleep(10);}catch(InterruptedException e){e.printStackTrace();}windowLock.lock();try{if(ticketCount > 0){System.out.println(Thread.currentThread().getName() + ": sale and left " + --ticketCount);}else{System.out.println(Thread.currentThread().getName() + ": sold out...");break;}}finally {windowLock.unlock();}}}
}
2. 条件变量机制
在synchronized
关键字中,我们可以通过wait¬ify
实现线程的等待与唤醒,但在此场景下存在虚假唤醒问题,根本原因就是其等待/唤醒机制只支持单条件(等待线程未作区分,只能全部唤醒)。相比之下,ReentrantLock
基于Condition
接口也实现了相同的机制,并提供了更细的粒度和更高级的功能;每个Condition
实例都对应一个条件队列,用于维护在该条件场景下等待通知的线程,并且ReentrantLock
支持多条件变量,即一个ReentrantLock
可以关联多个Condition
实例。其常用方法如下:
Type | Method | Description |
---|---|---|
void | await() throws InterruptedException | 使当前线程阻塞等待(进入该条件队列),并释放与此条件变量所关联的锁。注意: 若在等待期间被中断,则抛出InterruptedException ,并清除当前线程中断状态 |
boolean | await(long time,TimeUnit unit) throws InterruptedException | 使当前线程阻塞等待,并释放与此条件变量所关联的锁,直到被唤醒、被中断或等待time 时间过期。其返回值表示:- true : 在等待时间之内,条件被唤醒;- false : 等待时间过期,条件未被唤醒;注意: 若在等待期间被中断,则抛出 InterruptedException ,并清除当前线程中断状态 |
void | signal() | 唤醒一个等待在Condition 上的线程。如果有多个线程在此条件变量下等待,则选择任意一个线程唤醒;注意: 从等待方法返回前必须重新获得Condition 相关联的锁 |
void | signalAll() | 唤醒所有等待在Condition 上的线程。如果有多个线程在此条件变量下等待,则全部唤醒 ;注意: 从等待方法返回前必须重新获得Condition 相关联的锁 |
在使用时需要注意:
- 调用
await
相关方法前需要先获得对应条件变量所关联的锁,否则会抛出IllegalMonitorStateException
异常; - 调用
signal
相关方法前需要先获得对应条件变量所关联的锁,否则会抛出IllegalMonitorStateException
异常; await
线程被唤醒(或等待时间过期、被中断)后会重新参与锁的竞争,若成功拿到锁则将从await
处恢复继续向下执行;
/*** 场景模拟:奶茶店和咖啡店共用一个窗口(window)出餐,等待顾客点单...* - 奶茶店(teaWithMilk):顾客需要奶茶,则奶茶店开始工作;* - 咖啡店(coffee):顾客需要咖啡,则咖啡店开始工作;*/
public class Test {// 窗口锁(ReentrantLock实现)static final ReentrantLock window = new ReentrantLock();// 奶茶点单条件变量static Condition teaWithMilk = window.newCondition();// 咖啡点单条件变量static Condition coffee = window.newCondition();public static void main(String[] args) throws InterruptedException {// 奶茶店监控线程new Thread(new Runnable() {@Overridepublic void run() {while (true) {window.lock();try {System.out.println("[奶茶店] 等待接单...");try {teaWithMilk.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("[奶茶店] 接到订单...");} finally {window.unlock();}System.out.println("[奶茶店] 开始工作...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("[奶茶店] 工作完成...");}}}).start();Thread.sleep(1000);// 咖啡店监控线程new Thread(new Runnable() {@Overridepublic void run() {while (true) {window.lock();try {System.out.println("[咖啡店] 等待接单...");try {coffee.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("[咖啡店] 接到订单...");} finally {window.unlock();}System.out.println("[咖啡店] 开始工作...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("[咖啡店] 工作完成...");}}}).start();Thread.sleep(1000);// 顾客点单线程: mainwindow.lock();try{System.out.println("[顾客] 点了咖啡!!");coffee.signal(); // 唤醒咖啡条件等待线程}finally {window.unlock();}}
}
二. 从 ReentrantLock 分析 AQS 的原理
1. AQS 框架
AQS
全称为 AbstractQueuedSynchronizer
,即抽象队列同步器;AQS
是 java.util.concurrent.locks
包下的一个抽象类,其为构建锁和同步器提供了一系列通用模板与框架的实现,大部分JUC
包下的并发工具都是基于AQS
来构建的,比如ReentrantLock
、Semaphore
、CountDownLatch
等。其核心源码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {// 同步队列的节点static final class Node {...}// 指向同步队列头部private transient volatile Node head;// 指向同步队列尾部private transient volatile Node tail;// 同步状态private volatile int state;// 提供一系列并发、同步队列的基本操作方法// 比如: 挂起、取消、节点插入、节点替换等...// 交由子类实现的模板方法(钩子方法): 自定义同步器的核心实现目标protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}protected int tryAcquireShared(int arg) {throw new UnsupportedOperationException();}protected boolean tryReleaseShared(int arg) {throw new UnsupportedOperationException();}protected boolean isHeldExclusively() {throw new UnsupportedOperationException();}// 条件变量 Condition 接口的内部实现类public class ConditionObject implements Condition {...}}
由上可知,AQS内部实现了一个核心内部类Node
,该内部类表示对等待获取锁的线程的封装节点;在AQS中,基于Node
维护了一个双向链表(模拟同步队列),其中head
节点指向同步队列的头部,而tail
节点指向同步队列的尾部。Node
类的核心源码如下:
static final class Node {// 共享模式(共享锁、比如Semaphore)static final Node SHARED = new Node();// 独占模式(独占锁、比如ReentrantLock)static final Node EXCLUSIVE = null;// 标识节点线程获取锁的请求已取消、已结束static final int CANCELLED = 1;// 标识节点线程已准备就绪,等待被唤醒获取资源static final int SIGNAL = -1;// 标识节点线程在条件变量Condition中等待static final int CONDITION = -2;// 在共享模式下启用: 标识获得的同步状态会被传播static final int PROPAGATE = -3;/*** waitStatus 标识节点线程在同步队列中的状态,共存在以下几种情况:* (1)SIGNAL: 被标记为SIGNAL的节点处于等待唤醒获取资源的状态,只要前驱节点释放锁就会通知该状态的后续节点线程执行* (2)CANCELLED: 在同步队列中等待超时、被中断的线程会进入取消状态,不再响应并会在遍历过程中被移除* (3)CONDITION: 标识当前节点线程在Condition下等待,被唤醒后将重新从等待队列转移到同步队列* (4)PROPAGATE: 与共享模式有关 * (5)0: 默认初始值状态,代表节点初始化*/volatile int waitStatus;// 同步队列中的前驱节点volatile Node prev;// 同步队列中的后继节点volatile Node next;// 等待获取锁资源的线程volatile Thread thread;// Condition 等待队列中的后继节点(单向链表)Node nextWaiter;// 判断是否为共享模式final boolean isShared() {return nextWaiter == SHARED;}// 获取当前节点在同步队列中的前驱节点final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}// 省略构造方法...}
在了解Node
的基本数据结构与状态之后,AQS
还有一个核心状态变量即state
,该全局变量用于表示同步锁的状态,其具体含义一般在子类实现中进行定义和维护(独占锁和共享锁不一样)。但不管独占模式还是共享模式,简单来说都是使用一个volatile
的全局变量来表示资源同步状态(state
),并通过CAS完成对state
值的修改(修改成功则表示获锁成功),当持有锁的线程数量超过当前模式(独占模式一般限制为1)时,则通过内置的FIFO同步队列来完成资源获取线程的排队工作(通过LockSupport park/unpark
方法实现挂起与唤醒)。其核心原理图如下:
综上,AQS采用了模板方法模式来构建同步框架,并提供了一系列并发操作的公共基础方法,支持共享模式和独占模式两种实现;但AQS并不负责对外提供具体的加锁/解锁逻辑,因为锁是千变万化的,AQS只关注基础组件、顶层模板这些总的概念,具体的锁逻辑将通过”钩子“的方式下放给子类实现。也就是说,独占模式只需要实现tryAcquire-tryRelease
方法、共享模式只需要实现tryAcquireShared-tryReleaseShared
方法,搭配AQS提供的框架和基础组件就能轻松实现自定义的同步工具。
2. ReentrantLock 源码分析
由ReentrantLock
的类结构图可以看出,ReentrantLock
实现了Lock
接口,其内部包含一个内部类Sync
,该内部类继承了AQS
(AbstractQueuedSynchronizer
),ReentrantLock
大部分的锁操作都是通过Sync
实现的。除此之外,ReentrantLock
有公平锁和非公平锁两种模式,分别对应Sync
的FairSync
和NonfairSync
两个子类实现。
public class ReentrantLock implements Lock, java.io.Serializable {private final Sync sync;// 默认构造函数: 默认创建非公平锁public ReentrantLock() {sync = new NonfairSync();}// 带参构造函数: true公平锁/false非公平锁public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}// 加锁操作public void lock() {sync.lock();}//...
}
接下来,本节将首先以ReentrantLock
的非公平锁为例进行分析,然后再介绍公平锁与非公平锁的主要区别。
2.1 非公平锁lock加锁原理
非公平锁NonfairSync
的源码如下:
static final class NonfairSync extends Sync {// 加锁操作final void lock() {// CAS修改state状态以获取锁资源if (compareAndSetState(0, 1))// 成功则将独占锁线程设置为当前线程setExclusiveOwnerThread(Thread.currentThread());else// 否则再次请求同步状态(AQS的模板方法)acquire(1);}// AQS 获锁的钩子方法实现protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}
在调用lock
方法获取锁的过程中,当前线程会先通过CAS操作尝试修改state
从0
(表示无锁)到1
(表示占有锁),若修改成功则将AQS
中保存的独占线程exclusiveOwnerThread
修改为当前线程;若失败则执行acquire(1)
方法,该方法是AQS
中的一个模板方法,其源码如下:
public final void acquire(int arg) {// tryAcquire -> addWaiter -> acquireQueuedif (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
可以看到该方法首先调用了钩子方法tryAcquire
,该方法是交由子类NonfairSync
实现的,在上面的源码中我们已经给出了tryAcquire
的实现代码,其直接调用了父类Sync
中的nonfairTryAcquire
方法,其源码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {abstract void lock();// nonfairTryAcquire 方法实现final boolean nonfairTryAcquire(int acquires) {// 获取当前线程及同步队列状态statefinal Thread current = Thread.currentThread();int c = getState();// 若状态为0表示锁已释放: 重新尝试获取锁if (c == 0) {// CAS尝试修改state的值if (compareAndSetState(0, acquires)) {// 若成功则设置独占线程为当前线程setExclusiveOwnerThread(current);return true;}}// 若独占线程即当前线程,则属于重入锁else if (current == getExclusiveOwnerThread()) {// 修改state为重入值int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}//省略代码...
}
由上述代码可知,该方法首先再次判断锁是否已释放,这是为了避免之前持锁的线程在这段时间内又重新释放了锁,若state==0
则会尝试再次CAS修改同步状态以获取锁资源;否则的话,则判断当前线程是否是锁重入的情况,若两个判断都不满足则返回false
;到目前为止,我们回过头来想一下非公平锁的非公平性是在哪体现的?
很明显,在上述代码分析中,当有任何线程尝试获取锁时(调用lock
方法),不论当前同步队列中是否已有线程排队等待,NonfairSync
的lock()
方法以及Sync
的nonfairTryAcquire()
方法都没有对同步队列中的等待情况进行判断,而是直接通过CAS尝试修改state
的值来为当前线程直接占有锁;这就是非公平性的体现,抢占线程可以直接与等待线程竞争锁资源,而不用按照顺序加入队列。
分析完这部分之后,我们再回到acquire(1)
方法,若tryAcquire(arg)
方法返回false
即获取不到锁时会继续向下执行到addWaiter(Node.EXCLUSIVE)
方法,该方法用于封装线程入队,其源码如下:
private Node addWaiter(Node mode) {// 将请求占锁失败的线程封装为Node节点Node node = new Node(Thread.currentThread(), mode);Node pred = tail;// 若同步队列不为空,则尝试CAS在尾部插入当前节点(FIFO)if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 若队列为空或CAS插入节点失败则执行enq()方法处理入队enq(node);return node;}
接着,我们继续分析enq(node)
方法的实现:
private Node enq(final Node node) {// 开启自旋(循环)for (;;) {Node t = tail;// 若队列为空if (t == null) { // Must initialize// 则尝试CAS创建头节点(不存储数据)// 原因: 队列为空可能是因为其他线程非公平占有了锁(当前线程试过没抢到),因此这里需要先斩后奏,即再次创建头节点表示已占锁线程的占位,来维护同步队列if (compareAndSetHead(new Node()))tail = head;} else {// 否则尝试CAS添加尾节点node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}// 若CAS失败(可能有多线程并发操作),则不断自旋重试直到插入成功}
可以看到头节点head
其实是不存储数据的,它只表示一个线程占位(占位了锁资源),因为位于头节点的线程肯定已经获取到了锁,头节点只存储后继节点指向,用于当前线程释放锁资源时唤醒后继节点;那么到此这个方法也就分析完成了,在节点入队成功之后会返回当前节点node
,然后会继续执行到acquireQueued(addWaiter(Node.EXCLUSIVE),arg)
方法,其源码如下:
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false; //阻塞挂起标志// 开启自旋(循环)for (;;) {// 获取当前节点的前驱节点pfinal Node p = node.predecessor();// 若p是头节点则当前节点尝试获取锁资源if (p == head && tryAcquire(arg)) {// 占锁成功则设置当前节点node为头节点: 头节点状态保持SIGNAL状态setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 若p不是头节点或获取锁资源失败,则判断是否阻塞挂起线程来等待if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {// 若最终无法获取锁,则取消该线程的请求if (failed)cancelAcquire(node);}}// 将传递的节点设置为同步队列的头节点private void setHead(Node node) {head = node;// 清空当前节点存储的线程信息node.thread = null;node.prev = null;}
在acquireQueued
方法中,线程会开启自旋,若发现(或被唤醒后发现)当前节点的前驱节点变为头节点,则说明当前节点能够尝试获取锁资源,并尝试通过tryAcquire
方法获取同步状态;需要注意的是,head
头节点表示当前占有锁的线程节点,只有当head
节点对应的线程释放锁资源并唤醒后继节点时,后继节点线程才会自旋去尝试占有锁资源,因此:在同步队列中,只有前驱节点变为头节点时,当前节点才有资格尝试获取锁资源,其他时候都将被挂起等待,避免空转CPU
。
除此之外,若在自旋过程中,当前节点的前驱节点不是头节点或者节点尝试tryAcquire
获取锁资源失败,则会执行shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt()
逻辑;需要注意的是在前驱节点是头节点但尝试获取锁资源失败这种特殊情况发生时(比如非公平锁模式下被新到来的请求线程抢占),头节点head
此时可能有两种状态:
-
waitStatus==0
: 处于该状态一种情况是初始同步队列为空时,默认头节点状态初始化为0;另一种情况是锁释放时(见后文)被unparkSuccessor
重置头节点状态; -
waitStatus==SIGNAL
: 处于该状态一种情况是waitStatus==0
时更新状态后又自旋回来但仍未获取到锁(可能释放锁后被非公平抢占);另一种情况是释放锁时unparkSuccessor
重置失败;
其源码如下:
// 判断节点线程是否挂起等待private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取前驱节点的等待状态int ws = pred.waitStatus;// 若是SIGNAL状态,则说明前驱节点就绪,当前节点正常需要继续等待即返回trueif (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;// 若等待状态>0则说明前驱节点是结束状态,需要遍历前驱节点直到找到非结束状态的有效节点if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 若等待状态<=0且非SIGNAL,则尝试将前驱节点设置为SIGNAL/** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don't park yet. Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}// false 则返回去继续自旋return false;}// 执行挂起阻塞操作 LockSupport.parkprivate final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
shouldParkAfterFailedAcquire
方法的执行逻辑是判断前驱节点的等待状态waitStatus
,用于挂起当前节点线程等待,结合上述分析,包括以下几种情况:
- 前驱节点状态
waitStatus==SIGNAL
: 若前驱节点是头节点,则说明占锁线程还未执行结束,当前节点线程仍需挂起等待;若前驱节点不是头节点,则说明前驱节点就绪/拥有更高的优先级,下次执行还轮不到当前节点,所以也可以安全挂起,直接返回true
; - 前驱节点状态
waitStatus>0
: 说明前驱节点已处于结束/取消状态,应该从同步队列中移除,并遍历所有前驱节点直到找到非结束状态的有效节点作为前驱; - 前驱节点状态
waitStatus<0
且非SIGNAL: 前驱节点刚从Condition
的条件等待队列被唤醒,从而转移到同步队列,需要转换为SIGNAL
状态等待; - 前驱节点状态
waitStatus==0
:若前驱节点是头节点,则说明同步队列刚初始化(0)或锁刚被释放重置,锁资源可能未被其他线程持有,需判断能否占有锁(不管当前线程能否占有,该锁一定会被占有,都需要转换状态为SIGNAL
);若前驱节点不是头节点,则说明该线程节点刚初始化并被插入队列,需要转换为SIGNAL
状态;
综上,当shouldParkAfterFailedAcquire()
方法返回true
时会调用parkAndCheckInterrupt
方法挂起线程等待被唤醒,返回false
时则会继续自旋判断;至此,ReetrantLock
内部间接依靠AQS
的FIFO
同步队列,就完成了lock()
加锁操作。
2.2 公平锁lock加锁原理
公平锁FairSync
的源码如下:
static final class FairSync extends Sync {// 加锁操作final void lock() {acquire(1);}// AQS 获锁的钩子方法实现protected final boolean tryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取同步状态int c = getState();// 若当前没有线程持有锁资源if (c == 0) {// 首先判断同步队列是否存在等待节点if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 锁重入else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}
与非公平锁唯一不同的是:公平锁的tryAcquire
实现中,在尝试修改state
之前,会先调用hasQueuedPredecessors()
判断AQS
内部同步队列中是否已存在等待节点。如果存在,则说明在此之前,已经有线程提交了获取锁的请求,那么当前线程就会直接被封装成Node
节点,追加到队尾等待。
2.3 释放锁原理
ReetrantLock
显式锁需要手动释放锁资源,其unlock()
方法直接调用了Sync
中的release(1)
方法,而该方法又是在其父类AQS
中直接实现的,其源码如下:
public final boolean release(int arg) {// 尝试释放锁if (tryRelease(arg)) {// 进入该代码块则说明锁已完全释放(state=0)// 获取头节点Node h = head;if (h != null && h.waitStatus != 0)// 唤醒head头节点的后继节点线程unparkSuccessor(h);return true;}return false;}// 唤醒node的后继节点线程private void unparkSuccessor(Node node) {// 获取节点状态int ws = node.waitStatus;if (ws < 0) // 重置节点状态,允许失败compareAndSetWaitStatus(node, ws, 0);// 获取后继节点Node s = node.next;// 若后继节点为空或已结束if (s == null || s.waitStatus > 0) {s = null;// 寻找后继可被唤醒的有效等待节点for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 执行线程唤醒(继续去自旋) LockSupport.unparkif (s != null)LockSupport.unpark(s.thread);}
release
方法能否释放锁并唤醒后继节点线程依赖于tryRelease
钩子方法,而该方法又下放到了Sync
中实现,其源码如下:
// ReentrantLock -> Sync -> tryRelease(1)protected final boolean tryRelease(int releases) {// 计算释放锁后的同步更新状态int c = getState() - releases;// 如果当前释放锁的线程不为持有锁的线程则抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 判断更新状态是否为0,如果是则说明已完全释放锁if (c == 0) {free = true;// 完全释放锁才清空当前占有线程setExclusiveOwnerThread(null);}// 更新state值setState(c);// 完全释放锁才返回truereturn free;}
注意:tryRelease
方法执行完成返回true
之后,就说明当前线程持有的锁已被释放(非公平锁中就已经可以被抢占了),后续unparkSuccessor
方法只是进行一些善后工作,其中重置头节点状态的目的是表示逻辑上从持锁到无锁的转换,锁资源目前可能并没有线程持有,因此在后续线程唤醒后执行acquireQueued
自旋时waitStatus==0
状态会再一次判断并尝试获取锁,而修改为SIGNAL
就表示占锁线程正在执行,其他线程需要挂起等待。至此,整个流程可以结合起来理解:s
节点的线程被唤醒后,会继续执行acquireQueued()
方法中的自旋,判断if (p == head && tryAcquire(arg))
代码是否成立,从而执行判断操作。
三. 其他同步工具类
1. Semaphore
1.1 基本概述
Semaphore
是java.util.concurrent
包下的一种计数信号量,它同样也是基于AQS
实现的同步工具类。相比ReentrantLock
来说,它应该属于共享锁,即允许多个线程同时访问某个共享资源,但会限制同时访问特定资源的线程数量;Semaphore
同样也支持公平模式和非公平模式两种方式,其构造方法如下:
public Semaphore(int permits) {sync = new NonfairSync(permits);
}public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore
默认为非公平模式(抢占式),在构造信号量对象时都必须提供permits
参数;permits
可以理解为许可证数量,只有拿到许可证的线程才能执行,该参数限制了能同时获取或访问到共享资源的线程数量,其他超出线程都将阻塞等待。基于此,Semaphore
通常用于实现资源有明确访问数量限制的场景,比如限流、池化等。Semaphore
的常用方法介绍如下:
Type | Method | Description |
---|---|---|
void | acquire() throws InterruptedException | 当前线程请求获取该信号量的一个许可证。若有可用许可证,则获得许可证并返回执行同步代码,同时可用许可证数量将减少一个;若没有可用许可证,则阻塞等待直到有许可被释放,或线程中断。注意: 若当前线程在等待过程中被中断,则会抛出InterruptedException ,并清除当前线程的中断状态。 |
void | acquire(int permits) throws InterruptedException | 当前线程请求获取该信号量的permits 个许可证。若有可用数量的许可证,则获得许可证并返回执行同步代码,同时可用许可证数量将减少permits 个;若没有可用数量的许可证,则阻塞等待直到可用许可达到指定数量,或线程中断。**注意: **若当前线程在等待过程中被中断,则会抛出InterruptedException ,并清除当前线程的中断状态。 |
void | release() | 释放该信号量的一个许可证,并使可用许可证数量增加一个。注意: 没有通过acquire() 获取许可的线程甚至也可以直接调用release() 来为信号量增加许可证数量,并且可用许可有可能会超出构造时限制的permits 值,因此信号量的正确使用必须是通过应用程序中的编程约束来建立。 |
void | release(int permits) | 释放该信号量的permits 个许可证,并使可用许可证数量增加permits 个。注意: 同release() 方法,信号量的正确使用必须是通过应用程序中的编程约束来建立。 |
boolean | tryAcquire() | 尝试获取该信号量的一个许可证,但该方法会立即返回。若有可用许可证,则获得许可证并返回true ,同时可用许可证数量将减少一个;若没有可用许可证,则返回false 。注意: 该方法会破坏公平策略,对该方法的调用会进行抢占式获取(不管是否有线程在等待)。 |
boolean | tryAcquire(long timeout, TimeUnit unit) throws InterruptedException | 尝试在指定时间timeout 内获取该信号量的一个许可证(遵循公平策略)。若有可用许可证,则获得许可证并返回true ,同时可用许可证数量将减少一个;若没有可用许可证,则阻塞等待直到timeout 过期,等待时间过期则返回false 。注意: 若当前线程在等待过程中被中断,则会抛出InterruptedException ,并清除当前线程的中断状态。 |
int | availablePermits() | 获取当前信号量中的可用许可证数量。 |
可以看出,许可证是Semaphore
的核心概念,Semaphore
信号量对许可证的获取是强限制,但对许可证的释放是弱限制的,即请求线程在执行时必须获取acquire
到指定数量的许可证,但在释放release
时并不会对先前是否获取进行检查,因此可用许可有时可能会超出构造时限制的permits
值。换句话说,构造时传入的permits
参数只表示信号量的初始许可数量,并且许可证只决定了线程执行的门槛,但并不会对线程作全程限制;当前线程一旦获取到指定数量的许可便开始执行,即使中途释放许可也不会影响后续执行过程,这也就是为什么说信号量的正确使用必须是通过应用程序中的编程约束来建立。举例如下:
public class test {public static void main(String[] args) {// 初始化许可数量 = 5Semaphore semaphore = new Semaphore(5);new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + ": 等待许可...");semaphore.acquire(3); // acquire permits = 3System.out.println(Thread.currentThread().getName() + ": 拿到许可,剩余许可 = " + semaphore.availablePermits());Thread.sleep(3000);semaphore.release(); // release = 1System.out.println(Thread.currentThread().getName() + ": 释放许可...");} catch (InterruptedException e) {e.printStackTrace();}},"thread-1").start();try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + ": 等待许可...");semaphore.acquire(3);// acquire permits = 3System.out.println(Thread.currentThread().getName() + ": 拿到许可,剩余许可 = " + semaphore.availablePermits());Thread.sleep(3000);semaphore.release();// release = 1System.out.println(Thread.currentThread().getName() + ": 释放许可,剩余许可 = " + semaphore.availablePermits());} catch (InterruptedException e) {e.printStackTrace();}},"thread-2").start();}
}
thread-1: 等待许可...
thread-1: 拿到许可,剩余许可 = 2
thread-2: 等待许可...
thread-1: 释放许可...
thread-2: 拿到许可,剩余许可 = 0
thread-2: 释放许可,剩余许可 = 1
前文说过,Semaphore
通常用于实现资源有明确访问数量限制的场景,比如限流、池化等;此处通过Semaphore
模拟一个请求限流的场景,其中限制最大并发数为3,实现代码如下:
public class test {public static void main(String[] args) {// 自定义线程池ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2*2, 8,60, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(1024),new ThreadPoolExecutor.AbortPolicy());// 流量控制: 限制最大并发数为3final Semaphore semaphore = new Semaphore(3);// 模拟10个客户端任务请求for(int index = 0;index < 10;index++){final int serial = index;threadPool.execute(() -> {try {// 请求获取许可semaphore.acquire();System.out.println(Thread.currentThread().getName() + ": 请求成功!访问编号 = " + serial);// 模拟IO操作Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放许可semaphore.release();}});}// 等待线程池执行结束关闭: 不再接受新任务提交,对已经提交了的任务不会产生影响threadPool.shutdown();}
}
1.2 原理分析
Semaphore
的大部分方法都是基于内部类Sync
实现的,而该类又继承了 AbstractQueuedSynchronizer
即AQS
,并且Sync
对应的还有两个子类 NonfairSync
(非公平模式实现) 和 FairSync
(公平模式实现)。在Semaphore
中,AQS
的 state
被定义为 permits
(许可证数量),对象创建时传入的参数permits
实际是在对AQS内部的state
进行初始化,初始化完成后state
代表着当前信号量对象的可用许可数(state>0
)。
以非公平模式为例,当线程调用Semaphore.acquire(arg)
请求获取许可时,会首先判断remaining = getState() - arg
是否大于0,如果是则代表还有满足可用的许可数,并尝试对state
进行CAS操作使state=remaining
,若CAS成功则代表获取许可成功;否则线程需要封装成Node节点并加入同步队列阻塞等待,直到许可释放被唤醒。
// Semaphore类 -> acquire()方法
public void acquire() throws InterruptedException {// Sync类继承AQS,此处直接调用AQS内部的acquireSharedInterruptibly()方法sync.acquireSharedInterruptibly(1);}// AbstractQueuedSynchronizer类 -> acquireSharedInterruptibly()方法
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {// 判断是否出现线程中断信号(标志)if (Thread.interrupted())throw new InterruptedException();// 如果tryAcquireShared(arg)执行结果不小于0,则线程获取同步状态成功if (tryAcquireShared(arg) < 0)// 未获取成功加入同步队列阻塞等待doAcquireSharedInterruptibly(arg);
}
// Semaphore类 -> NofairSync内部类 -> tryAcquireShared()方法
protected int tryAcquireShared(int acquires) {// 调用了父类Sync中的实现方法return nonfairTryAcquireShared(acquires);
}// Syn类 -> nonfairTryAcquireShared()方法
abstract static class Sync extends AbstractQueuedSynchronizer {final int nonfairTryAcquireShared(int acquires) {// 开启自旋死循环for (;;) {int available = getState();int remaining = available - acquires;// 判断信号量中可用许可数是否已<0或者CAS执行是否成功if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}
}
释放逻辑对比获取许可的逻辑相对来说要简单许多,只需要更新state
值增加后调用doReleaseShared()
方法唤醒后继节点线程即可;需要注意的是,而在共享模式中可能会存在多条线程同时释放许可/锁资源,所以在此处使用了CAS+自旋
的方式保证线程安全问题。
2. CountDownLatch
2.1 基本概述
CountDownLatch
同样是java.util.concurrent
包下的基于AQS
实现的同步工具类。类似于Semaphore
,CountDownLatch
在初始化时也会传入一个参数count
来间接赋值给AQS
的state
,用于表示一个线程计数值;不过CountDownLatch
并没有构建公平模式和非公平模式(内部Sync
没有子类实现),其构造方法如下:
public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);
}
CountDownLatch
的主要作用是等待计数值count
归零后,唤醒所有的等待线程。基于该特性,CountDownLatch
常被用于控制多线程之间的等待与协作(多线程条件唤醒);相比join
来说,CountDownLatch
更加灵活且粒度更细,join
是以线程执行结束为条件,而CountDownLatch
是以方法的主动调用为条件。其常用方法如下:
Type | Method | Description |
---|---|---|
void | await() throws InterruptedException | 使当前线程阻塞等待,直到计数器count 归零或线程被中断。若当前计数已为零,则此方法立即返回。注意: 若在等待过程中被中断,则会抛出InterruptedException,并清除当前线程的中断状态。 |
void | countDown() | 使当前计数器count 递减。如果新计数归零,则唤醒所有await() 等待线程;注意: 若当前计数已为零,则无事发生。 |
long | getCount() | 获取当前计数器count 的值。 |
需要注意的是,CountDownLatch
是一次性的,即计数器的值count
只能在构造方法中初始化,此外再没有任何设置值的方法,当 CountDownLatch
使用完毕后(计数归零)将不能重复被使用;若需要重置计数的版本,可以考虑使用CyclicBarrier
。CountDownLatch
的常用方法有两种:
- 多等一:初始化
count=1
,多条线程await()
阻塞等待一条线程调用countDown()
唤醒所有线程。比如模拟并发安全、死锁等; - 一等多:初始化
count=N
,一条线程await()
阻塞等待N条线程调用countDown()
归零后唤醒。比如多接口调用的数据合并、多操作完成后的数据检查、主服务启动后等待多个组件加载完毕等(注意线程间的通信与数据传递需结合Future
实现);
public class test {public static void main(String[] args) {// 模拟10人拼团活动final CountDownLatch countDownLatch = new CountDownLatch(10);// 固定数量线程池ExecutorService threadPool = Executors.newFixedThreadPool(50);// 拼团人员ID集合List<String> ids = new ArrayList<>();// 模拟30人开始抢单拼团for (int i = 0; i < 30; i++) {threadPool.execute(() -> {boolean orderSucess = false;System.out.println(Thread.currentThread().getName() + ": 请求拼团...");if (countDownLatch.getCount() > 0) {synchronized (ids) {if (countDownLatch.getCount() > 0) {ids.add(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName() + ": 拼团成功!");countDownLatch.countDown();orderSucess = true;}}}if (!orderSucess) {System.out.println(Thread.currentThread().getName() + ": 拼团失败!已无名额...");}});}// 订单生成线程new Thread(() -> {try {countDownLatch.await();System.out.println(Thread.currentThread().getName() + ": 拼团结束, 订单已生成...");System.out.println(Thread.currentThread().getName() + ": 拼团人员id = " + ids);} catch (InterruptedException e) {e.printStackTrace();}}, "拼团").start();// 释放线程池threadPool.shutdown();}
}
2.2 原理分析
CountDownLatch
的底层实现原理也非常简单,当线程调用 await()
的时候,如果 state
不为 0 则证明任务还没有执行结束,await()
就会进入阻塞等待,其源码如下:
// CountDownLatch -> await()
public void await() throws InterruptedException {// 调用内部类sync的acquireSharedInterruptibly方法sync.acquireSharedInterruptibly(1);
}
// CountDownLatch -> Sync -> acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {// 被中断抛出异常if (Thread.interrupted())throw new InterruptedException();// tryAcquireShared -> 判断是否阻塞等待if (tryAcquireShared(arg) < 0)// 自旋+阻塞(AQS实现)doAcquireSharedInterruptibly(arg);
}
// CountDownLatch -> Sync -> tryAcquireShared
protected int tryAcquireShared(int acquires) {// 判断当前state是否归零return (getState() == 0) ? 1 : -1;
}
当线程调用 countDown()
时,其实最终是调用了Sync
中重写的tryReleaseShared
方法,该方法以 CAS 的操作来减少 state
;若更新后state
归零,则表示所有的计数任务线程都执行完毕,那么在 CountDownLatch
上等待的线程就会被AQS
的doReleaseShared
方法唤醒并继续向下执行。
// CountDownLatch -> countDown()
public void countDown() {sync.releaseShared(1);
}
// CountDownLatch -> Sync -> AQS -> releaseShared
public final boolean releaseShared(int arg) {// 判断递减后计数器是否归零if (tryReleaseShared(arg)) {// 唤醒所有等待线程(AQS实现)doReleaseShared();return true;}return false;
}
// CountDownLatch -> Sync -> tryReleaseShared
protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {// 获取当前stateint c = getState();// 若计数器归零则返回false,其他什么也不做if (c == 0)return false;// CAS更新state递减int nextc = c-1;if (compareAndSetState(c, nextc))// 若更新成功则判断新计数值是否归零return nextc == 0;}
}
3. CyclicBarrier
//
相关文章:
Java JUC(三) AQS与同步工具详解
Java JUC(三) AQS与同步工具详解 一. ReentrantLock 概述 ReentrantLock 是 java.util.concurrent.locks 包下的一个同步工具类,它实现了 Lock 接口,提供了一种相比synchronized关键字更灵活的锁机制。ReentrantLock 是一种独占…...
使用rust写一个Web服务器——async-std版本
文章目录 实现异步代码并发地处理连接使用多线程提升性能 使用rust实现一个异步运行时是async-std的单线程Web服务器。 仓库地址: 1037827920/web-server: 使用rust编写的简单web服务器 (github.com) 在之前的单线程版本的Web服务器代码上进行修改,具体…...
C语言复习概要(一)
本文 C语言入门详解:从基础概念到分支与循环1. C语言常见概念1.1 程序的基本结构1.2 变量作用域和存储类1.3 输入输出1.4 编译与运行 2. C语言中的数据类型和变量2.1 基本数据类型2.2 变量的声明与初始化2.3 常量与枚举 3. C语言的分支结构3.1 if语句3.2 if-else语句…...
二、kafka生产与消费全流程
一、使用java代码生产、消费消息 1、生产者 package com.allwe.client.simple;import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.pr…...
本地搭建OnlyOffice在线文档编辑器结合内网穿透实现远程协作
文章目录 前言1. 安装Docker2. 本地安装部署ONLYOFFICE3. 安装cpolar内网穿透4. 固定OnlyOffice公网地址 前言 本篇文章讲解如何使用Docker在本地Linux服务器上安装ONLYOFFICE,并结合cpolar内网穿透实现公网访问本地部署的文档编辑器与远程协作。 Community Editi…...
ScrapeGraphAI 大模型增强的网络爬虫
在数据驱动的动态领域,从在线资源中提取有价值的见解至关重要。从市场分析到学术研究,对特定数据的需求推动了对强大的网络抓取工具的需求。 NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线…...
PDF转换为TIF,JPG的一个简易工具(含下载链接)
目录 0.前言: 1.工具目录 2.工具功能(效果),如何运行 效果 PDF转换为JPG(带颜色) PDF转换为TIF(LZW形式压缩,可以显示子的深浅) PDF转换为TIF(CCITT形…...
Wireshark 解析QQ、微信的通信协议|TCP|UDP
写在前面 QQ,微信这样的聊天软件。我们一般称为im,Instant Messaging,即时通讯系统。那大家会不会有疑问,自己聊天内容会不会被黑客或者不法分子知道?这种体量的im是基于tcp还是udp呢?这篇文章我们就来探索…...
网络编程(5)——模拟伪闭包实现连接的安全回收
六、day6 今天学习如何利用C11模拟伪闭包实现连接的安全回收,之前的异步服务器为echo模式,但存在安全隐患,在极端情况下客户端关闭可能会导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构。今天学习如…...
C#绘制动态曲线
前言 用于实时显示数据动态曲线,比如:SOC。 //用于绘制动态曲线,可置于定时函数中,定时更新数据曲线 void DrawSocGraph() {double f (double)MainForm.readData[12]; //display datachart1.Series[0].Points.Add(f);if (ch…...
用Python实现运筹学——Day 10: 线性规划的计算机求解
一、学习内容 1. 使用 Python 的 scipy.optimize.linprog 进行线性规划求解 scipy.optimize.linprog 是 Python 中用于求解线性规划问题的函数。它实现了单纯形法、内点法等算法,能够处理求解最大化或最小化问题,同时满足线性约束条件。 线性规划问题的…...
[C++]使用C++部署yolov11目标检测的tensorrt模型支持图片视频推理windows测试通过
官方框架: https://github.com/ultralytics/ultralytics yolov8官方最近推出yolov11框架,标志着目标检测又多了一个检测利器,于是尝试在windows下部署yolov11的tensorrt模型,并最终成功。 重要说明:安装环境视为最基…...
霍夫曼树及其与B树和决策树的异同
霍夫曼树是一种用于数据压缩的二叉树结构,通常应用于霍夫曼编码算法中。它的主要作用是通过对符号进行高效编码,减少数据的存储空间。霍夫曼树在压缩领域扮演着重要角色,与B树、决策树等数据结构都有一些相似之处,但又在应用场景和…...
CompletableFuture常用方法
一、获得结果和触发计算 1.获取结果 (1)public T get() public class CompletableFutureAPIDemo{public static void main(String[] args) throws ExecutionException, InterruptedException{CompletableFuture<String> completableFuture Com…...
本地化测试对游戏漏洞修复的影响
本地化测试在游戏开发的质量保证过程中起着至关重要的作用,尤其是在修复bug方面。当游戏为全球市场做准备时,它们通常会被翻译和改编成各种语言和文化背景。这种本地化带来了新的挑战,例如潜在的语言错误、文化误解,甚至是不同地区…...
使用rust实现rtsp码流截图
中文互联网上的rust示例程序源码还是太稀少,找资料很是麻烦,下面是自己用业余时间开发实现的一个对批量rtsp码流源进行关键帧截图并存盘的rust demo源码记录。 要编译这个源码需要先安装vcpkg,然后用vcpkg install ffmpeg安装最新版本的ffmpe…...
Cpp::STL—string类的模拟实现(12)
文章目录 前言一、string类各函数接口总览二、默认构造函数string(const char* str "");string(const string& str);传统拷贝写法现代拷贝写法 string& operator(const string& str);传统赋值构造现代赋值构造 ~string(); 三、迭代器相关函数begin &…...
一文搞懂SentencePiece的使用
目录 1. 什么是 SentencePiece?2. SentencePiece 基础概念2.1 SentencePiece 的工作原理2.2 SentencePiece 的优点 3. SentencePiece 的使用3.1 安装 SentencePiece3.2 训练模型与加载模型3.3 encode(高频)3.4 decode(高频&#x…...
一个简单的摄像头应用程序1
这个Python脚本实现了一个基于OpenCV的简单摄像头应用,我们在原有的基础上增加了录制视频等功能,用户可以通过该应用进行拍照、录制视频,并查看已拍摄的照片。以下是该脚本的主要功能和一些使用时需要注意的事项: 功能 拍照: 用户可以通过点击界面上的“拍照”按钮或按…...
通过PHP获取商品详情
在电子商务的浪潮中,数据的重要性不言而喻。商品详情信息对于电商运营者来说尤为宝贵。PHP,作为一种广泛应用的服务器端脚本语言,为我们提供了获取商品详情的便捷途径。 了解API接口文档 开放平台提供了详细的API接口文档。你需要熟悉商品详…...
【Android】获取备案所需的公钥以及签名MD5值
目录 重要前提 获取签名MD5值 获取公钥 重要前提 生成jks文件以及gradle配置应用该文件。具体步骤请参考我这篇文章:【Android】配置Gradle打包apk的环境_generate signed bundle or apk-CSDN博客 你只需要从头看到该文章的配置build.gradle(app&…...
看480p、720p、1080p、2k、4k、视频一般需要多大带宽呢?
看视频都喜欢看高清,那么一般来说看电影不卡顿需要多大带宽呢? 以4K为例,这里引用一位网友的回答:“视频分辨率4092*2160,每个像素用红蓝绿三个256色(8bit)的数据表示,视频帧数为60fps,那么一秒钟画面的数据量是:4096*2160*3*8*60≈11.9Gbps。此外声音大概是视频数据量…...
解决IDEA中@Autowired红色报错的实用指南:原因与解决方案
前言: 在使用Spring Boot开发时,Autowired注解是实现依赖注入的常用方式。然而,许多开发者在IDEA中使用Autowired时,可能会遇到红色报错,导致代码的可读性降低。本文将探讨导致这种现象的原因,并提供几种解…...
408知识点自检(一)
一、细节题 虚电路是面向连接的吗?虚电路线路上会不会有其他虚电路通过?虚电路适合什么类型的数据交换?虚电路的可靠性靠其他协议还是自己?固态硬盘的优势体现在什么存取方式?中断向量地址是谁的地址?多播…...
负载均衡--相关面试题(六)
在负载均衡的面试中,可能会遇到一系列涉及概念、原理、实践应用以及技术细节的问题。以下是一些常见的负载均衡面试题及其详细解答: 一、什么是负载均衡? 回答:负载均衡是一种将网络请求或数据传输工作分配给多个服务器或网络资源…...
【Unity踩坑】Unity更新Google Play结算库
一、问题描述: 在Google Play上提交了app bundle后,提示如下错误。 我使用的是Unity 2022.01.20f1,看来用的Play结算库版本是4.0 查了一下文档,Google Play结算库的维护周期是两年。现在需要更新到至少6.0。 二、更新过程 1. 下…...
Redis:hash类型
Redis:hash类型 hash命令设置与读取HSETHGETHMGET 哈希操作HEXISTSHDELHKEYSHVALSHGETALLHLENHSETNXHINCRBYHINCRBYFLOAT 内部编码ziplisthashtable 目前主流的编程语言中,几乎都提供了哈希表相关的容器,Redis自然也会支持对应的内容…...
力扣9.30
1749. 任意子数组和的绝对值的最大值 给你一个整数数组 nums 。一个子数组 [numsl, numsl1, ..., numsr-1, numsr] 的 和的绝对值 为 abs(numsl numsl1 ... numsr-1 numsr) 。 请你找出 nums 中 和的绝对值 最大的任意子数组(可能为空),…...
kafka下载配置
下载安装 参开kafka社区 zookeeperkafka消息队列群集部署https://apache.csdn.net/66c958fb10164416336632c3.html 下载 kafka_2.12-3.2.0安装包快速下载地址分享 官网下载链接地址: 官网下载地址:https://kafka.apache.org/downloads 官网呢下载慢…...
nlp任务之预测中间词-huggingface
目录 1.加载编码器 1.1编码试算 2.加载数据集 3.数据集处理 3.1 map映射:只对数据集中的sentence数据进行编码 3.2用filter()过滤 单词太少的句子过滤掉 3.3截断句子 4.创建数据加载器Dataloader 5. 下游任务模型 6.测试预测代码 7.训练代码 8.保…...
基于php技术的网站开发/百度seo软件是做什么的
柜体是柜子的重要组成部分,柜体用什么材料?今日就由PChouse为你一一解答。1、实木颗粒板实木颗粒板是一种新型、高档环保的基材,技术已经很成熟了。它是由木材或其他木质纤维素材料制成的碎料,两边使用细密木纤维中间夹长质木纤维…...
汕头制作网站推荐/磁力搜索引擎下载
接上篇四、DNS名称更新在默认情况下,运行Windows 2000而且静态配置TCP/IP的计算机尝试为由其安装的网络连接所配置和使用的IP地址动态注册主机(A)和指针(PTR)资源记录(RR)。在默认情况下,所有计算机都在其“完整计算机名”的基础上注册记录。下列任何原因…...
asp网站上传后台在哪/发广告平台有哪些
题目: 1、给一数组,所有的数都出现了两次,只有一个数出现了一次,求这个数。 2、给一数组,所有的数都出现了两次,只有两个数出现了一次, 求这两个数。 思路: 位运算 1、将数组里所有的…...
wordpress导入数据库结构/求几个微信推广平台
我有一个硬件设备可以在我的网络上发送多播数据。我已经编写了一个python脚本来接收数据并打印出来。但是,我发现它只在我的windowsxp电脑上运行,在我的ubuntulinux10.04pc上不起作用。它只是绕过while循环,从来没有收到任何数据。我的代码张…...
松江 网站建设公司/全球搜索引擎入口
《GAPNet: Graph Attention-based Point Neural Network for Exploiting Local Feature of Point Cloud》 研究问题如何更好地提取点云的局部特征主要想法 采用self-attention和neighboring attention的机制,一方面通过self-attetion来提取输入点自身的几何特征&am…...
2017做电商做什么网站/域名注册网站系统
一、JSTL技术 1.JSTL概述 JSTL(JSP Standard Tag Library),JSP标准标签库,可以嵌入在jsp页面中使用标签的形式完成业务逻辑等功能。jstl出现的目的同el一样也是要提到jsp页面中的脚本代码。JSTL标准标准标签库有5个子库࿰…...