JUC第十四讲:JUC锁: ReentrantReadWriteLock详解
JUC第十四讲:JUC锁: ReentrantReadWriteLock详解
本文是JUC第十四讲:JUC锁 - ReentrantReadWriteLock详解。ReentrantReadWriteLock表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLock,可以通过这两种锁实现线程间的同步。
文章目录
- JUC第十四讲:JUC锁: ReentrantReadWriteLock详解
- 1、带着BAT大厂的面试问题去理解
- 2、ReentrantReadWriteLock数据结构
- 3、ReentrantReadWriteLock源码分析
- 3.1、类的继承关系
- 3.2、类的内部类
- 3.3、内部类 - Sync类
- 3.4、内部类 - Sync核心函数分析
- 3.5、类的属性
- 3.6、类的构造函数
- 3.7、核心函数分析
- 4、ReentrantReadWriteLock示例
- 5、更深入理解
- 5.1、什么是锁升降级?
- 6、参考文章
1、带着BAT大厂的面试问题去理解
请带着这些问题继续后文,会很大程度上帮助你更好的理解相关知识点。
- 为了有了ReentrantLock还需要ReentrantReadWriteLock? 实现读锁共享,写锁互斥
- ReentrantReadWriteLock底层实现原理? ReentrantLock + AQS
- ReentrantReadWriteLock底层读写状态如何设计的? 高16位为读锁,低16位为写锁
- 读锁和写锁的最大数量是多少? 2^16
- 本地线程计数器ThreadLocalHoldCounter是用来做什么的? 线程与对象相关联
- 缓存计数器HoldCounter是用来做什么的? 记录锁的可重入次数
- 写锁的获取与释放是怎么实现的?
- 读锁的获取与释放是怎么实现的?
- 什么是锁的升降级? RentrantReadWriteLock为什么不支持锁升级? 保证可见性
2、ReentrantReadWriteLock数据结构
ReentrantReadWriteLock底层是基于ReentrantLock和AbstractQueuedSynchronizer来实现的,所以,ReentrantReadWriteLock的数据结构也依托于AQS的数据结构。
3、ReentrantReadWriteLock源码分析
3.1、类的继承关系
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}
说明: 可以看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到 ReentrantReadWriteLock 实现了自己的序列化逻辑。
3.2、类的内部类
ReentrantReadWriteLock 有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。
说明: 如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
3.3、内部类 - Sync类
- 类的继承关系
abstract static class Sync extends AbstractQueuedSynchronizer {}
说明:Sync抽象类继承自AQS抽象类,Sync类提供了对 ReentrantReadWriteLock 的支持。
- 类的内部类
Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用,其中,HoldCounter源码如下。
// 计数器
static final class HoldCounter {// 计数int count = 0;// Use id, not reference, to avoid garbage retention// 获取当前线程的TID属性的值final long tid = getThreadId(Thread.currentThread());
}
说明: HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。ThreadLocalHoldCounter的源码如下
// 本地线程计数器
static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {// 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值public HoldCounter initialValue() {return new HoldCounter();}
}
说明: ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是 initialValue 方法里面生成的那个 HolderCounter 对象。
- 类的属性
abstract static class Sync extends AbstractQueuedSynchronizer {// 版本序列号private static final long serialVersionUID = 6317671515068378041L;// 高16位为读锁,低16位为写锁static final int SHARED_SHIFT = 16;// 读锁单位 2^16static final int SHARED_UNIT = (1 << SHARED_SHIFT);// 读锁最大数量 2^16 - 1static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;// 写锁最大数量 2^16 - 1static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 本地线程计数器private transient ThreadLocalHoldCounter readHolds;// 缓存的计数器private transient HoldCounter cachedHoldCounter;// 第一个读线程private transient Thread firstReader = null;// 第一个读线程的计数private transient int firstReaderHoldCount;
}
说明:该属性中包括了读锁、写锁线程的最大量。本地线程计数器等。
- 类的构造函数
// 构造函数
Sync() {// 本地线程计数器readHolds = new ThreadLocalHoldCounter();// 设置AQS的状态setState(getState()); // ensures visibility of readHolds
}
说明:在Sync的构造函数中设置了本地线程计数器和AQS的状态state。
3.4、内部类 - Sync核心函数分析
对 ReentrantReadWriteLock 对象的操作绝大多数都转发至Sync对象进行处理。下面对Sync类中的重点函数进行分析
- sharedCount 函数
表示占有读锁的线程数量,源码如下
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
说明:直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的低十六位表示写锁数量。
- exclusiveCount函数
表示占有写锁的线程数量,源码如下
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
说明: 直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示。
- tryRelease函数
/*
* Note that tryRelease and tryAcquire can be called by
* Conditions. So it is possible that their arguments contain
* both read and write holds that are all released during a
* condition wait and re-established in tryAcquire.
*/protected final boolean tryRelease(int releases) {// 判断是否为独占线程if (!isHeldExclusively())throw new IllegalMonitorStateException();// 计算释放资源后的写锁的数量int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0; // 是否释放成功if (free)setExclusiveOwnerThread(null); // 设置独占线程为空setState(nextc); // 设置状态return free;
}
说明:此函数用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其函数流程图如下。
- tryAcquire 函数
protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero* and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only* happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if* it is either a reentrant acquire or* queue policy allows it. If so, update state* and set owner.*/// 获取当前线程Thread current = Thread.currentThread();// 获取状态int c = getState();// 写线程数量int w = exclusiveCount(c);if (c != 0) { // 状态不为0// (Note: if c != 0 and w == 0 then shared count != 0)if (w == 0 || current != getExclusiveOwnerThread()) // 写线程数量为0或者当前线程没有占有独占资源return false;if (w + exclusiveCount(acquires) > MAX_COUNT) // 判断是否超过最高写线程数量throw new Error("Maximum lock count exceeded");// Reentrant acquire// 设置AQS状态setState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires)) // 写线程是否应该被阻塞return false;// 设置独占线程setExclusiveOwnerThread(current);return true;
}
说明:此函数用于获取写锁,首先会获取state,判断是否为0,若为0,表示此时没有读锁线程,再判断写线程是否应该被阻塞,而在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),之后在设置状态state,然后返回true。若state不为0,则表示此时存在读锁或写锁线程,若写锁线程数量为0或者当前线程为独占锁线程,则返回false,表示不成功,否则,判断写锁线程的重入次数是否大于了最大值,若是,则抛出异常,否则,设置状态state,返回true,表示成功。其函数流程图如下
- tryReleaseShared 函数
protected final boolean tryReleaseShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();// 当前线程为第一个读线程if (firstReader == current) {// assert firstReaderHoldCount > 0;// 读线程占用的资源数为1if (firstReaderHoldCount == 1) firstReader = null;else // 减少占用的资源firstReaderHoldCount--;} else { // 当前线程不为第一个读线程// 获取缓存的计数器HoldCounter rh = cachedHoldCounter;// 计数器为空 或者计数器的tid不为当前正在运行的线程的tidif (rh == null || rh.tid != getThreadId(current))// 获取当前线程对应的计数器rh = readHolds.get();// 获取计数int count = rh.count;if (count <= 1) { // 计数小于等于1// 移除readHolds.remove();if (count <= 0) // 计数小于等于0,抛出异常throw unmatchedUnlockException();}// 减少计数--rh.count;}// 无限循环for (;;) {// 获取状态int c = getState();// 获取状态int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc)) // 比较并进行设置// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}
}
说明:此函数表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。其流程图如下
- tryAcquireShared函数
private IllegalMonitorStateException unmatchedUnlockException() {return new IllegalMonitorStateException("attempt to unlock read lock, not locked by current thread");
}// 共享模式下获取资源
protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for* lock wrt state, so ask if it should block* because of queue policy. If not, try* to grant by CASing state and updating count.* Note that step does not check for reentrant* acquires, which is postponed to full version* to avoid having to check hold count in* the more typical non-reentrant case.* 3. If step 2 fails either because thread* apparently not eligible or CAS fails or count* saturated, chain to version with full retry loop.*/// 获取当前线程Thread current = Thread.currentThread();// 获取状态int c = getState();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current) // 写线程数不为0并且占有资源的不是当前线程return -1;// 读锁数量int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) { // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功if (r == 0) { // 读锁数量为0// 设置第一个读线程firstReader = current;// 读线程占用的资源数为1firstReaderHoldCount = 1;} else if (firstReader == current) { // 当前线程为第一个读线程// 占用资源数加1firstReaderHoldCount++;} else { // 读锁数量不为0并且不为当前线程// 获取计数器HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid// 获取当前线程对应的计数器cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0) // 计数为0// 设置readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);
}
说明:此函数表示读锁线程获取读锁。首先判断写锁是否为0 并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下:
- fullTryAcquireShared函数
final int fullTryAcquireShared(Thread current) {/** This code is in part redundant with that in* tryAcquireShared but is simpler overall by not* complicating tryAcquireShared with interactions between* retries and lazily reading hold counts.*/HoldCounter rh = null;for (;;) { // 无限循环// 获取状态int c = getState();if (exclusiveCount(c) != 0) { // 写线程数量不为0if (getExclusiveOwnerThread() != current) // 不为当前线程return -1;// else we hold the exclusive lock; blocking here// would cause deadlock.} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞// Make sure we're not acquiring read lock reentrantly// 当前线程为第一个读线程if (firstReader == current) {// assert firstReaderHoldCount > 0;} else { // 当前线程不为第一个读线程if (rh == null) { // 计数器不为空// rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tidrh = readHolds.get();if (rh.count == 0)readHolds.remove();}}if (rh.count == 0)return -1;}}if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常throw new Error("Maximum lock count exceeded");if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功if (sharedCount(c) == 0) { // 读线程数量为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; // cache for release}return 1;}}
}
说明:在 tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功) 则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。其逻辑与tryAcquireShared逻辑类似,不再累赘。
而其他内部类的操作基本上都是转化到了对Sync对象的操作,在此不再累赘。
3.5、类的属性
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {// 版本序列号private static final long serialVersionUID = -6992448646407690164L; // 读锁private final ReentrantReadWriteLock.ReadLock readerLock;// 写锁private final ReentrantReadWriteLock.WriteLock writerLock;// 同步队列final Sync sync;private static final sun.misc.Unsafe UNSAFE;// 线程ID的偏移地址private static final long TID_OFFSET;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> tk = Thread.class;// 获取线程的tid字段的内存地址TID_OFFSET = UNSAFE.objectFieldOffset(tk.getDeclaredField("tid"));} catch (Exception e) {throw new Error(e);}}
}
说明: 可以看到ReentrantReadWriteLock属性包括了一个ReentrantReadWriteLock.ReadLock对象,表示读锁;一个ReentrantReadWriteLock.WriteLock对象,表示写锁;一个Sync对象,表示同步队列。
3.6、类的构造函数
- ReentrantReadWriteLock()型构造函数
public ReentrantReadWriteLock() {// 默认非公平锁this(false);
}
说明:此构造函数会调用另外一个有参构造函数。
- ReentrantReadWriteLock(boolean) 型构造函数
public ReentrantReadWriteLock(boolean fair) {// 公平策略或者是非公平策略sync = fair ? new FairSync() : new NonfairSync();// 读锁readerLock = new ReadLock(this);// 写锁writerLock = new WriteLock(this);
}
说明: 可以指定设置公平策略或者非公平策略,并且该构造函数中生成了读锁与写锁两个对象。
3.7、核心函数分析
对ReentrantReadWriteLock的操作基本上都转化为了对Sync对象的操作,而Sync的函数已经分析过,不再累赘。
4、ReentrantReadWriteLock示例
下面给出了一个使用ReentrantReadWriteLock的示例,源代码如下。
import java.util.concurrent.locks.ReentrantReadWriteLock;class ReadThread extends Thread {private ReentrantReadWriteLock rrwLock;public ReadThread(String name, ReentrantReadWriteLock rrwLock) {super(name);this.rrwLock = rrwLock;}public void run() {System.out.println(Thread.currentThread().getName() + " trying to lock");try {rrwLock.readLock().lock();System.out.println(Thread.currentThread().getName() + " lock successfully");Thread.sleep(5000); } catch (InterruptedException e) {e.printStackTrace();} finally {rrwLock.readLock().unlock();System.out.println(Thread.currentThread().getName() + " unlock successfully");}}
}class WriteThread extends Thread {private ReentrantReadWriteLock rrwLock;public WriteThread(String name, ReentrantReadWriteLock rrwLock) {super(name);this.rrwLock = rrwLock;}public void run() {System.out.println(Thread.currentThread().getName() + " trying to lock");try {rrwLock.writeLock().lock();System.out.println(Thread.currentThread().getName() + " lock successfully"); } finally {rrwLock.writeLock().unlock();System.out.println(Thread.currentThread().getName() + " unlock successfully");}}
}public class ReentrantReadWriteLockDemo {public static void main(String[] args) {ReentrantReadWriteLock rrwLock = new ReentrantReadWriteLock();ReadThread rt1 = new ReadThread("rt1", rrwLock);ReadThread rt2 = new ReadThread("rt2", rrwLock);WriteThread wt1 = new WriteThread("wt1", rrwLock);rt1.start();rt2.start();wt1.start();}
}
运行结果(某一次):
rt1 trying to lock
rt2 trying to lock
wt1 trying to lock
rt1 lock successfully
rt2 lock successfully
rt1 unlock successfully
rt2 unlock successfully
wt1 lock successfully
wt1 unlock successfully
说明: 程序中生成了一个ReentrantReadWriteLock对象,并且设置了两个读线程,一个写线程。根据结果,可能存在如下的时序图。
- rt1线程执行 rrwLock.readLock().lock 操作,主要的函数调用如下。
说明:此时,AQS的状态state为2^16 次方,即表示此时读线程数量为1。
- rt2线程执行 rrwLock.readLock().lock 操作,主要的函数调用如下。
说明: 此时,AQS的状态state为2 * 2^16次方,即表示此时读线程数量为2。
- wt1线程执行rrwLock.writeLock().lock操作,主要的函数调用如下。
说明:此时,在同步队列Sync queue中存在两个结点,并且wt1线程会被禁止运行。
- rt1线程执行 rrwLock.readLock().unlock 操作,主要的函数调用如下。
说明:此时,AQS的state为 2^16次方,表示还有一个读线程。
- rt2线程执行 rrwLock.readLock().unlock 操作,主要的函数调用如下。
说明:当rt2线程执行unlock操作后,AQS的state为0,并且wt1线程将会被unpark,其获得CPU资源就可以运行。
- wt1线程获得CPU资源,继续运行,需要恢复。由于之前 acquireQueued 函数中的 parkAndCheckInterrupt函数 中被禁止的,所以,恢复到 parkAndCheckInterrupt函数中,主要的函数调用如下:
说明:最后,sync queue队列中只有一个结点,并且头节点尾节点均指向它,AQS的state值为1,表示此时有一个写线程。
- wt1执行 rrwLock.writeLock().unlock 操作,主要的函数调用如下。
说明:此时,AQS的state为0,表示没有任何读线程或者写线程了。并且Sync queue结构与上一个状态的结构相同,没有变化。
在项目中的使用?
- todo
5、更深入理解
5.1、什么是锁升降级?
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
接下来看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作,如代码如下所示:
// update变量使用volatile修饰
public void processData() {readLock.lock();if (!update) {// 必须先释放读锁readLock.unlock();// 锁降级从写锁获取到开始writeLock.lock();try {if (!update) {// 准备数据的流程(略)update = true;}readLock.lock();} finally {writeLock.unlock();}// 锁降级完成,写锁降级为读锁}try {// 使用数据的流程(略)} finally {readLock.unlock();}
}
上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问 processData() 方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。
锁降级中读锁的获取是否必要呢? 答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
6、参考文章
- 【JUC】JDK1.8源码分析之ReentrantReadWriteLock
- ReentrantReadWriteLock
相关文章:

JUC第十四讲:JUC锁: ReentrantReadWriteLock详解
JUC第十四讲:JUC锁: ReentrantReadWriteLock详解 本文是JUC第十四讲:JUC锁 - ReentrantReadWriteLock详解。ReentrantReadWriteLock表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLockÿ…...

在vue3中使用vite-svg-loader插件
vite-svg-loader插件可以让我们像使用vue组件那样使用svg图,使用起来超级方便。 安装 npm install vite-svg-loader --save-dev使用 import svgLoader from vite-svg-loaderexport default defineConfig({plugins: [vue(), svgLoader()] })组件里使用 在路径后加…...

国庆10.4
QT实现TCP服务器客户端 服务器 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer> //服务器头文件 #include <QTcpSocket> //客户端头文件 #include <QList> //链表容器 #include <QMe…...

2023/8/12 下午8:41:46 树状控件guilite
2023/8/12 下午8:41:46 树状控件guilite 2023/8/12 下午8:42:08 树状控件(Tree View)是一种常见的图形用户界面(GUI)元素,它通常用于显示层次结构数据或文件系统的目录结构。Guilite 是一个轻量级的跨平台 GUI 库,支持多种控件,包括树状控件。 在 Guilite 中使用树状…...

BL808学习日志-2-LVGL for M0 and D0
一、lvgl测试环境 对拿到的M1S_DOCK开发板进行开发板测试,博流的官方SDK是支持M0和D0两个内核都进行测试的;但是目前只实现了M0的LVGLBenchmark,测试D0内核中发现很多莫名其妙的问题。一会详细记录。 使用的是开发板自带的SPI显示屏ÿ…...

treectrl类封装 2023/8/13 下午4:07:35
2023/8/13 下午4:07:35 treectrl类封装 2023/8/13 下午4:07:53 TreeCtrl 类是一个常用的图形用户界面控件,用于实现树形结构的展示和交互。以下是一个简单的 TreeCtrl 类的封装示例: python import wxclass MyTreeCtrl(wx.TreeCtrl):def __init__(self, parent):super()…...

Android学习之路(20) 进程间通信
IPC IPC为 (Inter-Process Communication) 缩写,称为进程间通信或跨进程通信,指两个进程间进行数据交换的过程。安卓中主要采用 Binder 进行进程间通信,当然也支持其他 IPC 方式,如:管道,Socket࿰…...

机器学习——KNN算法流程详解(以iris为例)
、 目 录 前情说明 问题陈述 数据说明 KNN算法流程概述 代码实现 运行结果 基于可视化的改进 可视化代码 全部数据可视化总览 分类投票结果 改进后最终代码 前情说明 本书基于《特征工程入门与入门与实践》庄家盛 译版P53页K最近邻(KNN)算…...

国庆假期day5
作业:请写出七层模型及每一层的功能,请绘制三次握手四次挥手的流程图 1.OSI七层模型: 应用层--------提供函 表示层--------表密缩 会话层--------会话 传输层--------进程的接收和发送 网络层--------寻主机 数据链路层----相邻节点的可靠传…...

ES6中的let、const
let ES6中新增了let命令,用来声明变量,和var类似但是也有一定的区别 1. 块级作用域 只能在当前作用域内使用,各个作用域不能互相使用,否则会报错。 {let a 1;var b 1; } console.log(a); // 会报错 console.log(b); // 1为什…...

Python 列表操作指南3
示例,将新列表中的所有值设置为 ‘hello’: newlist [hello for x in fruits]表达式还可以包含条件,不像筛选器那样,而是作为操纵结果的一种方式: 示例,返回 “orange” 而不是 “banana”: …...

三个要点,掌握Spring Boot单元测试
单元测试是软件开发中不可或缺的重要环节,它用于验证软件中最小可测试单元的准确性。结合运用Spring Boot、JUnit、Mockito和分层架构,开发人员可以更便捷地编写可靠、可测试且高质量的单元测试代码,确保软件的正确性和质量。 一、介绍 本文…...

【nginx】Nginx配置:
文章目录 一、什么是Nginx:二、为什么使用Nginx:三、如何处理请求:四、什么是正向代理和反向代理:五、nginx 启动和关闭:六、目录结构:七、配置文件nginx.conf:八、location:九、单页…...

CSS3与HTML5
box-sizing content-box:默认,宽高包不含边框和内边距 border-box:也叫怪异盒子,宽高包含边框和内边距 动画:移动translate,旋转、transform等等 走马灯:利用动画实现animation:from…...

redis的简单使用
文章目录 环境安装与配置redis发布-订阅相关命令redis发布-订阅的客户端编程redis的订阅发布的例子 环境安装与配置 sudo apt-get install redis-server # ubuntu命令安装redis服务ubuntu通过上面命令安装完redis,会自动启动redis服务,通过ps命令确认&a…...

Windows下启动freeRDP并自适应远端桌面大小
几个二进制文件 xfreerdp # Linux下的,an X11 Remote Desktop Protocol (RDP) client which is part of the FreeRDP project wfreerdp.exe # Windows下的,freerdp2.0 主程序,freerdp3.0将废弃 sdl-freerdp.exe # Windows下的&…...

ES6中的数值扩展
1. 二进制和八进制的表示法 二进制和八进制的前缀分别为0b(或0B)和0o(或0O)表示 在ES5的严格模式下,八进制不再允许使用前缀0表示 如果要将0b和0x前缀的字符串数值转为十进制,要使用Number方法 Number(0b111); // 7 Number(0o10); // 82. Number.isF…...

自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能
自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能 文章目录 [toc] 1.依赖2.Redisson配置2.1单机模式配置2.2主从模式2.3集群模式2.4哨兵模式 3.实现3.1 RedisConfig3.2 自定义注解IdempotentManualCtrlTransLimiterAnno3.3自定义切面Idempote…...

Linux:minishell
目录 1.实现逻辑 2.代码及效果展示 1.打印字符串提示用户输入指令 2.父进程拆解指令 3.子进程执行指令,父进程等待结果 4.效果 3.实现过程中遇到的问题 1.打印字符串的时候不显示 2.多换了一行 3.cd路径无效 4.优化 1.ll指令 2.给文件或目录加上颜色 代码链接 模…...

STM32驱动步进电机
前言 (1)本章介绍用stm32驱动42步进电机,将介绍需要准备的硬件器材、所需芯片资源以及怎么编程及源代码等等。 (2)实验效果:按下按键,步进电机顺时针或逆时针旋转90度。 (3ÿ…...

计算机视觉——飞桨深度学习实战-深度学习网络模型
深度学习网络模型的整体架构主要数据集、模型组网以及学习优化过程三部分,本章主要围绕着深度学习网络模型的算法架构、常见模型展开了详细介绍,从经典的深度学习网络模型以CNN、RNN为代表,到为了解决显存不足、实时性不够等问题的轻量化网络…...

用c动态数组(不用c++vector)实现手撸神经网咯230901
用c语言动态数组(不用c++的vector)实现:输入数据inputs = { {1, 1}, {0,0},{1, 0},{0,1} };目标数据targets={0,0,1,1}; 测试数据 inputs22 = { {1, 0}, {1,1},{0,1} }; 构建神经网络,例如:NeuralNetwork nn({ 2, 4,3,1 }); 则网络有四层、输入层2个nodes、输出层1个节点、第…...

视频讲解|基于DistFlow潮流的配电网故障重构代码
目录 1 主要内容 2 视频链接 1 主要内容 该视频为基于DistFlow潮流的配电网故障重构代码讲解内容,对应的资源下载链接为基于DistFlow潮流的配电网故障重构(输入任意线路),对该程序进行了详尽的讲解,基本做到句句分析和讲解(讲解…...

Ultralytics(YoloV8)开发环境配置,训练,模型转换,部署全流程测试记录
关键词:windows docker tensorRT Ultralytics YoloV8 配置开发环境的方法: 1.Windows的虚拟机上配置: Python3.10 使用Ultralytics 可以得到pt onnx,但无法转为engine,找不到GPU,手动转也不行࿰…...

springboot之@ImportResource:导入Spring配置文件~
ImportResource的作用是允许在Spring配置文件中导入其他的配置文件。通过使用ImportResource注解,可以将其他配置文件中定义的Bean定义导入到当前的配置文件中,从而实现配置文件的模块化和复用。这样可以方便地将不同的配置文件进行组合,提高…...

阿里云服务器免费申请入口_注册阿里云免费领4台服务器
注册阿里云账号,免费领云服务器,最高领取4台云服务器,每月750小时,3个月免费试用时长,可快速搭建网站/小程序,部署开发环境,开发多种企业应用。阿里云百科分享阿里云服务器免费领取入口、免费云…...

ES6中的async、await函数
async是为了解决异步操作,其实是一个语法糖,使代码书写更加简洁。 1. async介绍 async放在一个函数的前面,await则放在异步操作前面。async代表这个函数中有异步操作需要等待结果,在一个async函数中可以存在多个await࿰…...

代码随想录算法训练营第五十六天 | 动态规划 part 14 | 1143.最长公共子序列、1035.不相交的线、53. 最大子序和(dp)
目录 1143.最长公共子序列思路代码 1035.不相交的线思路代码 53. 最大子序和(dp)思路代码 1143.最长公共子序列 Leetcode 思路 本题和718. 最长重复子数组 区别在于这里不要求是连续的了,但要有相对顺序,即:“ace” …...

【数据挖掘】2021年 Quiz 1-3 整理 带答案
目录 Quiz 1Quiz 2Quiz 3Quiz 1 Problem 1 (30%). Consider the training data shown below. Here, A A A and B B B</...

【软件设计师-中级——刷题记录6(纯干货)】
目录 管道——过滤器软件体系结构风格优点:计算机英语重点词汇:单元测试主要检查模块的以下5个特征:数据库之并发控制中的事务:并发产生的问题解决方案:封锁协议原型化开发方法: 每日一言:持续更新中... 个…...