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 有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

img

说明: 如上图所示,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,表示成功释放,资源不将被占用,否则,表示资源还被占用。其函数流程图如下。
img

  • 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,表示成功。其函数流程图如下

img

  • 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。其流程图如下

img

  • 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对象的值。流程图如下:
img

  • 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对象,并且设置了两个读线程,一个写线程。根据结果,可能存在如下的时序图。
img

  • rt1线程执行 rrwLock.readLock().lock 操作,主要的函数调用如下。

img

说明:此时,AQS的状态state为2^16 次方,即表示此时读线程数量为1

  • rt2线程执行 rrwLock.readLock().lock 操作,主要的函数调用如下。

img

说明: 此时,AQS的状态state为2 * 2^16次方,即表示此时读线程数量为2。

  • wt1线程执行rrwLock.writeLock().lock操作,主要的函数调用如下。

img

说明:此时,在同步队列Sync queue中存在两个结点,并且wt1线程会被禁止运行。

  • rt1线程执行 rrwLock.readLock().unlock 操作,主要的函数调用如下。

img

说明:此时,AQS的state为 2^16次方,表示还有一个读线程。

  • rt2线程执行 rrwLock.readLock().unlock 操作,主要的函数调用如下。

img

说明:当rt2线程执行unlock操作后,AQS的state为0,并且wt1线程将会被unpark,其获得CPU资源就可以运行。

  • wt1线程获得CPU资源,继续运行,需要恢复。由于之前 acquireQueued 函数中的 parkAndCheckInterrupt函数 中被禁止的,所以,恢复到 parkAndCheckInterrupt函数中,主要的函数调用如下:

img

说明:最后,sync queue队列中只有一个结点,并且头节点尾节点均指向它,AQS的state值为1,表示此时有一个写线程

  • wt1执行 rrwLock.writeLock().unlock 操作,主要的函数调用如下。
    img

说明:此时,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

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

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

相关文章

Day-05 CentOS7.5 安装docker

参考 &#xff1a; Install Docker Engine on CentOS | Docker DocsLearn how to install Docker Engine on CentOS. These instructions cover the different installation methods, how to uninstall, and next steps.https://docs.docker.com/engine/install/centos/ Doc…

实战型开发--3/3,clean code

编程的纯粹 hmmm&#xff0c;一开始在这个环节想聊一些具体的点&#xff0c;其实也就是《clean code》这本书中的点&#xff0c;但这个就还是更流于表面&#xff1b; 因为编码的过程&#xff0c;就更接近于运动员打球&#xff0c;艺术家绘画&#xff0c;棋手下棋的过程&#x…

GPT系列论文解读:GPT-1

GPT系列 GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一系列基于Transformer架构的预训练语言模型&#xff0c;由OpenAI开发。以下是GPT系列的主要模型&#xff1a; GPT&#xff1a;GPT-1是于2018年发布的第一个版本&#xff0c;它使用了12个Transformer…

软件设计师_计算机网络_学习笔记

文章目录 4.1 网路技术标准与协议4.1.1 协议4.1.2 DHCP4.1.3 DNS的两种查询方式 4.2 计算机网络的分类4.2.1 拓扑结构 4.3 网络规划与设计4.3.1 遵循的原则4.3.2 逻辑网络设计4.3.3 物理网络设计4.3.4 分层设计 4.4 IP地址与子网划分4.4.1 子网划分4.4.2 特殊IP 4.5 HTML4.6 无…

【MySQL】表的约束(二)

文章目录 一. 主键二. 自增长三. 唯一值四. 外键结束语 一. 主键 主键primary key 用来唯一的约束该字段里面的数据&#xff0c;不能重复&#xff0c;不能为空&#xff0c;一张表最多只能有一个主键&#xff0c;主键所在的列通常是整数类型 创建表时直接在字段上指定主键 mysq…

在2023年使用Unity2021从Built-in升级到Urp可行么

因为最近在做WEbgl平台&#xff0c;所以某些不可抗力原因&#xff0c;需要使用Unity2021开发&#xff0c;又由于不可明说原因&#xff0c;想用Urp&#xff0c;怎么办&#xff1f; 目录 创建RenderAsset 关联Asset 暴力转换&#xff08;Menu->Edit&#xff09; 单个文件…

【服务器】在 Linux CLI 下安装 Anaconda

【服务器】在 Linux CLI 下安装 Anaconda 1 系统环境2 下载安装包3 安装 1 系统环境 查看系统信息 cat /etc/os-release2. 查看架构 uname -a # output # Linux localhost.localdomain 4.18.0-193.28.1.el8_2.x86_64 #1 SMP Thu Oct 22 00:20:22 UTC 2020 x86_64 x86_64 x86…

华为云云耀云服务器L实例评测|Docker部署及应用

文章目录 前言&#x1f4e3; 1.前言概述&#x1f4e3; 2.服务器攻击✨ 2.1 问题描述✨ 2.2 处理方法 &#x1f4e3; 3.Docker简介&#x1f4e3; 4.安装Docker✨ 4.1 卸载旧版docker✨ 4.2 安装依赖包✨ 4.3 安装GPG证书✨ 4.4 配置仓库✨ 4.5 正式安装Docker✨ 4.6 配置用户组✨…

PG 多表连接查询

写法&#xff1a; 使用 select 表名.键名 from 表1 join表2 on 相同的主键 构造出来一张新表 多表要用表名.键名 才能知道是哪一张表 传统写法也行 类型&#xff1a; 内 而外的要这样写

动态规划-状态机(188. 买卖股票的最佳时机 IV)

状态分类&#xff1a; f[i,j,0]考虑前i只股票&#xff0c;进行了j笔交易&#xff0c;目前未持有股票 所能获得最大利润 f[i,j,1]考虑前i只股票&#xff0c;进行了j笔交易&#xff0c;目前持有股票 所能获得最大利润 状态转移&#xff1a; f[i][j][0] Math.max(f[i-1][j][0],f[…

lv7 嵌入式开发-网络编程开发 07 TCP服务器实现

目录 1 函数介绍 1.1 socket函数 与 通信域 1.2 bind函数 与 通信结构体 1.3 listen函数 与 accept函数 2 TCP服务端代码实现 3 TCP客户端代码实现 4 代码优化 5 练习 1 函数介绍 其中read、write、close在IO中已经介绍过&#xff0c;只需了解socket、bind、listen、acc…

Qt+openCV学习笔记(十六)Qt6.6.0rc+openCV4.8.1+emsdk3.1.37编译静态库

前言&#xff1a; 有段时间没来写文章了&#xff0c;趁编译库的空闲&#xff0c;再写一篇记录文档 WebAssembly的发展逐渐成熟&#xff0c;即便不了解相关技术&#xff0c;web前端也在不经意中使用了相关技术的库&#xff0c;本篇文档记录下如何编译WebAssembly版本的openCV&…

13.(开发工具篇github)如何在GitHub上上传本地项目

一:创建GitHub账户并安装Git 二:创建一个新的仓库(repository) 三、拉取代码 git clone https://github.com/ainier-max/myboot.git git clone git@github.com:ainier-max/myboot.git四、拷贝代码到拉取后的工程 五、上传代码 (1)添加所有文件到暂存

Mac安装Ecplise产品报错:dose not contain the JNI_CreateJavaVM symbol

1. 絮絮叨叨 工作中需要借助Ecplise Memory Analyzer (MAT)分析dump文件&#xff0c;直接下载、安装、运行MAT报错 询问同事后&#xff0c;同事说可以先安装Ecplise&#xff0c;再以插件的形式安装MAT下载、安装好Eclipse&#xff0c;点击运行仍然报错&#xff0c;且错误信息一…

【C语言】循环结构程序设计 (详细讲解)

前言&#xff1a;前面介绍了程序中常常用到的顺序结构和选择结构&#xff0c;但是只有这两种结构是不够的&#xff0c;还有用到循环结构(或者称为重复结构)。因为在日常生活中或是在程序所处理的问题中常常遇到需要重复处理的问题。 【卫卫卫的代码仓库】 【选择结构】 【专栏链…

springmvc-JSR303进行服务端校验分组验证SpringMVC定义Restfull接口异常处理流程RestController异常处理

目录& 1. JSR303 2. JSR303中含有的注解 3. spring中使用JSR303进行服务端校验 3.1 导入依赖包 3.2 添加验证规则 3.3 执行校验 4. 分组验证 4.1 定义分组验证规则 4.2 验证时通过参数指定验证规则 4.3 验证信息的显示 5. SpringMVC定义Restfull接口 5.1 增加s…

分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测

分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测 目录 分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现PSO-CNN多特征分类预测&#xff0c;多特征输入模型&#xf…

Webpack 基础入门以及接入 CSS、Typescript、Babel

一、什么是 Webpack Webpack 是一款 JS 模块化开发的技术框架&#xff0c;其运作原理是将多个 JS 文件关联起来构成可运行的应用程序。 Webpack 拥有丰富的 plugins / loaders 插件生态圈&#xff0c;可以让 js 识别不同的语言如 .css, .scss, .sass, .json, .xml, .ts, .vue…

Web版Photoshop来了,用到了哪些前端技术?

经过 Adobe 工程师多年来的努力&#xff0c;并与 Chrome 等浏览器供应商密切合作&#xff0c;通过 WebAssembly Emscripten、Web Components Lit、Service Workers Workbox 和新的 Web API 的支持&#xff0c;终于在近期推出了 Web 版 Photoshop&#xff08;photoshop.adobe…

BL808学习日志-2-LVGL for M0 and D0

一、lvgl测试环境 对拿到的M1S_DOCK开发板进行开发板测试&#xff0c;博流的官方SDK是支持M0和D0两个内核都进行测试的&#xff1b;但是目前只实现了M0的LVGLBenchmark&#xff0c;测试D0内核中发现很多莫名其妙的问题。一会详细记录。 使用的是开发板自带的SPI显示屏&#xff…