并发编程-05AQS原理

并发编程-深入理解AQS之ReentrantLock

在这里插入图片描述

一 认识AQS

在讲解AQS原理以及相关同步器之前,我们需要对AQS有一些基本的认识,了解下它有什么样的机制,这样追踪源码的时候就不会太过于迷茫!

1.1 什么是AQS

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于 AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

image-20230706123223141

JDK中提供的大多数的同步器如Lock, Latch, Barrier等都是基于AQS框架来实现的

一般实现套路为:

  • 一般是通过一个内部类Sync继承 AQS
  • 将AQS方法的调用都映射到Sync对应的方法

1.2 AQS的特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

1.3 AQS信号灯

AQS内部维护了一个变量state来维持加锁和解锁,表示资源的可用状态,它是volatile修饰的

state有三种访问方式

  • getState()
  • setState()
  • compareAndSetState()

1.4 AQS对共享资源的访问方式

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

1.5 AQS中的两种等待队列

  • 同步等待队列: 主要用于维护获取锁失败时入队的线程。

    AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。

    AQS 依赖CLH同步队列来完成同步状态的管理:

    • 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程。
    • 当锁释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
    • 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。
  • 条件等待队列:

    AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:

    • 调用await方法阻塞线程
    • 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

1.7 Condition接口

image-20230707090159881

  1. 调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
  2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

1.6 AQS队列节点状态

AQS 定义了5个队列中节点状态:

  • 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  • CANCELLED,值为1,表示当前的线程被取消;
  • SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
  • CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  • PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行

1.7 自定义同步器

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

了解了一下AQS的基础知识后,现在应该脑子里还是很懵懵的,不知道这些东西到底是怎么结合起来来实现线程同步的,接下来通过ReentrantLock这个同步锁来分析下它是怎么基于AQS实现同步的。

二 ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

相对于 synchronized, ReentrantLock具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 支持可重入

这里先总结下ReentrantLock和synchronized的区别

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
  • synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
  • synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
  • 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显式释放锁;
  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  • synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;

2.1 ReentrantLock的基本使用

1.非公平锁

ReentrantLock lock = new ReentrantLock();

ReentrantLock lock = new ReentrantLock(false);

public class NoFairLockDemo {public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "t" + i).start();}// 1s 之后去争抢锁Thread.sleep(1000);for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "强行插入" + i).start();}}
}

运行结果:

image-20230707124402662

第一个循环的500个线程是排队执行的,但是第二个循环中的线程却可以抢占到锁,说明后来的线程是可以不用排队直接尝试去竞争锁的。

2. 公平锁

只需要把参数设置为true即可

ReentrantLock lock = new ReentrantLock(true);

image-20230707170136189

可以看到,第二个循环里的线程是等到第一个循环里的线程都释放锁后才开始排队执行。

3.可重入

public class ReentrantLockDemo1 {static ReentrantLock  lock = new ReentrantLock();public static void main(String[] args) {new Thread(()->{lock.lock();try {System.out.println("第一次获得锁");method2();}finally {lock.unlock();}}).start();}public static void method2(){lock.lock();try {System.out.println("第二次获得锁");}finally {lock.unlock();}}}

运行结果:

image-20230707170936187

4.可打断

public class ReentrantLockDemo2 {public static void main(String[] args) {ReentrantLock  lock = new ReentrantLock();Thread t1 = new Thread(() -> {System.out.println("线程1启动");try {lock.lockInterruptibly();try {System.out.println("线程1获取锁");} finally {lock.unlock();}} catch (InterruptedException e) {e.printStackTrace();System.out.println("线程1等待锁被打断");}}, "t1");lock.lock();try {System.out.println("main线程获取线程");t1.start();//先让线程t1执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t1.interrupt();System.out.println("打断线程1");} finally {lock.unlock();}}}

运行结果:

image-20230707171850621

5.锁超时

public class ReentrantLockDemo3 {public static void main(String[] args) throws InterruptedException{ReentrantLock  lock = new ReentrantLock();Thread t1 = new Thread(() -> {// 注意: 即使是设置的公平锁,此方法也会立即返回获取锁成功或失败,公平策略不生效try {if (!lock.tryLock(1000,TimeUnit.MILLISECONDS)) {System.out.println("线程1等待超时");return;}} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}, "线程1");lock.lock();try {System.out.println("main线程获取锁成功");t1.start();//先让线程t1执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}} finally {lock.unlock();}}  
}

6.条件变量

@Slf4j
public class ReentrantLockDemo4 {private static Lock lock = new ReentrantLock();//是否有烟private static boolean hasCig = false;//外卖是否送到private static boolean hasTake = false;private static Condition cigCon  = lock.newCondition();private static Condition takeCon  = lock.newCondition();public static void main(String[] args) throws InterruptedException {ReentrantLockDemo4 test = new ReentrantLockDemo4();new Thread(() ->{test.cigratee();}).start();new Thread(() ->{lock.lock();try {hasCig = true;//唤醒送烟的等待线程cigCon.signal();}finally {lock.unlock();}},"t1").start();Thread.sleep(300L);new Thread(() -> {test.takeout();}).start();new Thread(() ->{lock.lock();try {hasTake = true;//唤醒送饭的等待线程takeCon.signal();}finally {lock.unlock();}},"t2").start();}//送烟public void cigratee(){lock.lock();try {while(!hasCig){try {log.debug("没有烟,歇一会");cigCon.await();}catch (Exception e){e.printStackTrace();}}log.debug("有烟了,干活");}finally {lock.unlock();}}//送外卖public void takeout(){lock.lock();try {while(!hasTake){try {log.debug("没有饭,歇一会");takeCon.await();}catch (Exception e){e.printStackTrace();}}log.debug("有饭了,干活");}finally {lock.unlock();}}
}

运行结果:

image-20230711123443914

2.2 ReentrantLock的原理

在学习源码的时候,我们需要把握重点去学习,比如ReentrantLock实现同步的原理,加锁和解锁的原理,公平锁和非公平锁的原理,线程入队原理,线程出队原理,可打断原理、条件变量原理。

1.继承关系

image-20230711124342128

可以看到ReentrantLock提供了两个同步器,分别实现公平锁和非公平锁,默认是非公平锁!

2.公平锁和非公平锁创建

这一块我们从ReentrantLock的构造方法来看

无参构造:

    public ReentrantLock() {sync = new NonfairSync();}

有参构造:

    public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

从构造方法来看,不传参默认创建非公平锁。

3.加锁逻辑(lock方法)

非公平锁
        final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}
公平锁
        final void lock() {acquire(1);}

从这一块看出来,非公平锁是来竞争的线程都直接先进行一次cas操作尝试是否能够获取锁,竞争失败才会调用acquire(1)方法,而公平锁是直接进行调用acquire(1)方法,而加锁失败阻塞的逻辑主要在acquire(1)方法中

acquire(1)方法

acquire(1)方式是由AbstractQueuedSynchronizer同步锁实现,代码如下

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

先说明下这几个方法的意思:

tryAcquire(arg):再次尝试竞争锁,

addWaiter(Node.EXCLUSIVE):构建队列

acquireQueued():阻塞线程

AbstractQueuedSynchronizer并没有直接实现tryAcquire方法,而是交给子类自行扩展,对于ReentrantLock来说,它的公平锁和非公平锁实现是一样的,我们看下tryAcquire的具体实现:

        final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

这个方式也是ReentrantLock锁可重入的体现,首先判断信号量是否为0,为0的话再次去进行cas获取锁,不为0的话,判断获取锁的线程是否是自己,是自己的话把信号量加1,并返回true。

假设我们竞争锁失败,返回false,进入第二个判断 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),我们需要先看addWaiter(Node.EXCLUSIVE)这个方法是干嘛的,这里多了一个Node类型,我们看下Node是一个什么样的数据结构

      	//共享模式static final Node SHARED = new Node();//独占模式static final Node EXCLUSIVE = null;static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;volatile int waitStatus;volatile Node prev;volatile Node next;volatile Thread thread;

很明显,Node是一个双向链表的数据结构,接下来我们看下addWaiter这个方法的逻辑

    private Node addWaiter(Node mode) {//构建一个Node节点Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}

这里我们先顺着逻辑往下看,第一个竞争锁的线程pred为空,肯定会进入enq(node)方法:

    private Node enq(final Node node) {for (;;) {Node t = tail; //t是尾节点,只有第一次的时候既是头节点也是尾节点if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

tail仍然为空,通过cas操作,新建一个头节点,这就是并发的精髓了,通过一个死循环,第二次循环的时候tail不为空,进入else逻辑,把当前线程所在的节点的前驱节点指向前边的结点,并把当前线程节点设置为尾结点。(这里通过cas保证线程安全问题),构建完队列,发现我们的线程还没有阻塞,玄机就在acquireQueued方法中

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {//获取前驱节点final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

这里假设调用tryAcquire方法竞争锁失败,进入第二个if判断,仍然是两个逻辑判断:

shouldParkAfterFailedAcquire(p, node);

parkAndCheckInterrupt());

先看shouldParkAfterFailedAcquire(p, node)

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;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 {/** 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);}return false;}

这里判断很简单,就是判断当前线程节点的前置节点是否是-1,是-1就返回true,否则,就把前一个节点的状态设置为-1,第二轮循环判断是-1,则返回true,进入parkAndCheckInterrupt())方法

    private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

这里就进行了线程的阻塞,并返回打断标志,到这里,我们的入队流程就结束了。

4.解锁逻辑(unlock方法)

    public void unlock() {sync.release(1);}

release方法是AbstractQueuedSynchronizer同步器来实现的,那么出队逻辑也是在AbstractQueuedSynchronizer中实现的

    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

tryRelease是解锁方法,这里是由同步器子类自定义实现

        protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}

这里也是锁可重入的解锁流程,每次减1,直到把state减到0,将线程置为空,然后进入if逻辑中,现在就是出队的逻辑了,主要是unparkSuccessor方法

    private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/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;}if (s != null)LockSupport.unpark(s.thread);}

这里通过unpark直接唤醒的是头节点的下一个线程,那么到这里,我们就要回到入队的acquireQueued方法这里(因为之前的线程是在这个方法阻塞的)

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

这里会进入第一个if中,设置当前节点为新的头结点,并清空线程。

5.可打断原理

先看不可打断的park逻辑

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;// 需要获得锁后, 才能返回打断状态return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//因为interrupt被唤醒, 返回打断状态为 true,但是此时还是会在队列中parkinterrupted = true;}} finally {if (failed)cancelAcquire(node);}}

再看看可打断的park逻辑

    private void doAcquireInterruptibly(int arg)throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//抛出异常throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}

当其他线程调用interrupt唤醒阻塞的线程的时候,这里直接抛出异常。

6.条件变量原理

t arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;😉 {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}


当其他线程调用interrupt唤醒阻塞的线程的时候,这里直接抛出异常。### 6.条件变量原理

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

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

相关文章

LabVIEW与OpenCV图像处理对比

LabVIEW和OpenCV在图像处理方面各有特点。LabVIEW擅长图形化编程、实时处理和硬件集成&#xff0c;而OpenCV则提供丰富的算法和多语言支持。通过DLL、Python节点等方式&#xff0c;OpenCV的功能可在LabVIEW中实现。本文将结合具体案例详细分析两者的特点及实现方法。 LabVIEW与…

某大会的影响力正在扩大,吞噬了整个数据库世界!

1.规模空前 你是否曾被那句“上有天堂&#xff0c;下有苏杭”所打动&#xff0c;对杭州的湖光山色心驰神往&#xff1f;7月&#xff0c;正是夏意正浓的时节&#xff0c;也是游览杭州的最佳时期。这座古典与现代交融的城市将迎来了第13届PostgreSQL中国技术大会。作为全球数据库…

LabVIEW从测试曲线中提取特征值

在LabVIEW中开发用于从测试曲线中提取特征值的功能时&#xff0c;可以考虑以下几点&#xff1a; 数据采集与处理&#xff1a; 确保你能够有效地采集和处理测试曲线数据。这可能涉及使用DAQ模块或其他数据采集设备来获取曲线数据&#xff0c;并在LabVIEW中进行处理和分析。 特…

系统级别的原生弹窗窗口

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>原生的弹出窗口dialog</title><style>…

【TB作品】51单片机 Proteus仿真 基于单片机的LCD12864万年历及温度监测系统设计

实验报告&#xff1a;基于单片机的LCD12864万年历及温度监测系统设计 背景介绍 本实验旨在设计并实现一个基于STC89C52单片机的LCD12864显示的万年历系统&#xff0c;同时集成温度传感器以实现温度监测功能。系统具备整点报时和闹钟功能&#xff0c;通过蜂鸣器进行提示。该设…

初中物理知识点总结(人教版)

初中物理知识点大全 声现象知识归纳 1 .声音的发生&#xff1a;由物体的振动而产生。振动停止&#xff0c;发声也停止。 2.声音的传播&#xff1a;声音靠介质传播。真空不能传声。通常我们听到的声音是靠空气传来的。 3.声速&#xff1a;在空气中传播速度是&#xff1a;340…

【2024_CUMCM】T检验、F检验、卡方检验

T检验 T检验主要用于比较两组数据的均值差异&#xff0c;适用于小样本数据分析。它可以分为单样本T检验、独立样本T检验和配对样本T检验。 单样本T检验用于比较一个样本与已知的总体均值差异&#xff0c;独立样本T检验用于比较两个独立样本的均值差异&#xff0c;配对样本T检…

【Transformer】transformer模型结构学习笔记

文章目录 1. transformer架构2. transformer子层解析3. transformer注意力机制4. transformer部分释疑 图1 transformer模型架构 图2 transformer主要模块简介 图3 encoder-decoder示意图N6 图4 encoder-decoder子层示意图 1. transformer架构 encoder-decoder框架是一种处理NL…

探索InitializingBean:Spring框架中的隐藏宝藏

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索MYSQL索引数据结构之旅✨ &#x1f44b; Spring框架的浩瀚海洋中&#x…

Java里的Arrary详解

DK 中提供了一个专门用于操作数组的工具类&#xff0c;即Arrays 类&#xff0c;位于java.util 包中。该类提供了一些列方法来操作数组&#xff0c;如排序、复制、比较、填充等&#xff0c;用户直接调用这些方法即可不需要自己编码实现&#xff0c;降低了开发难度。 java.util.…

用PlantUML和语雀画UML类图

概述 首先阐述一下几个简单概念&#xff1a; UML&#xff1a;是统一建模语言&#xff08;Unified Modeling Language&#xff09;的缩写&#xff0c;它是一种用于软件工程的标准化建模语言&#xff0c;旨在提供一种通用的方式来可视化软件系统的结构、行为和交互。UML由Grady…

pyrender 离线渲染包安装教程

pyrender 离线渲染包安装教程 安装 安装 官方安装教程:https://pyrender.readthedocs.io/en/latest/install/index.html#installmesa 首先 pip install pyrenderclang6.0安装 下载地址:https://releases.llvm.org/download.html#6.0.0 注意下好是叫&#xff1a;clangllvm-6…

通信协议_C#实现自定义ModbusRTU主站

背景知识&#xff1a;modbus协议介绍 相关工具 mbslave:充当从站。虚拟串口工具:虚拟出一对串口。VS2022。 实现过程以及Demo 打开虚拟串口工具: 打开mbslave: 此处从站连接COM1口。 Demo实现 创建DLL库&#xff0c;创建ModbusRTU类,进行实现&#xff1a; using Syste…

VMware虚拟机搭建CentOS7环境

相关资料 安装VMware 双击VMware-workstation(16.1.1软件安装包.exe安装文件,点下一步 激活码文件复制激活码激活安装linux 1、点击创建虚拟机

【MySQL基础篇】多表查询

1、多表关系 概述&#xff1a;项目开发中&#xff0c;在进行数据库表结构操作设计时&#xff0c;会根据业务需求及业务模板之间的关系&#xff0c;分析并设计表结构&#xff0c;由于业务之间相互关联&#xff0c;所以各个表结构之间也存在着各种联系&#xff0c;基本上分为三种…

从FasterTransformer源码解读开始了解大模型(2.1)代码通读03

从FasterTransformer源码解读开始了解大模型&#xff08;2.2&#xff09;代码解读03-forward函数 写在前面的话 本篇的内容继续解读forward函数&#xff0c;从650行开始进行解读 零、输出Context_embeddings和context_cum_log_probs的参数和逻辑 从653行开始&#xff0c;会…

架构师学习理解和总结

1.架构设计理念 2.架构方法论 2.1需求分析 2.1.1常见需求层次 2.1.2 常见需求结果 2.1.3 需求与架构关系 2.2 领域分析 2.3 关键需求 2.4 概念架构设计 2.5 细化架构设计 2.6 架构设计验证 3.架构设计工具 3.1 DDD领域建模 3.2 41视图分析法 3.3 UML设计工具 4.架构师知…

系统化学习 H264视频编码(01)基础概念

说明&#xff1a;我们参考黄金圈学习法&#xff08;什么是黄金圈法则?->模型 黄金圈法则&#xff0c;本文使用&#xff1a;why-what&#xff09;来学习音H264视频编码。本系列文章侧重于理解视频编码的知识体系和实践方法&#xff0c;理论方面会更多地讲清楚 音视频中概念的…

Swift 中的方法调用机制

Swift 方法调用详解&#xff1a;与 Objective-C 的对比、V-Table 机制、Witness Table 机制 在 iOS 开发中&#xff0c;Swift 和 Objective-C 是两种常用的编程语言。尽管它们都能用于开发应用程序&#xff0c;但在方法调用的底层机制上存在显著差异。本文将详细介绍 Swift 的…

实验2 Aprori关联挖掘算法

目 录 一、实验目的... 1 二、实验环境... 1 三、实验内容... 1 3.1 connect_string()函数解析... 1 3.2 find_rule()函数解析纠错... 2 3.3 关联规则挖掘... 4 四、心得体会... 7 一、实验目的 &#xff08;1&#xff09;理解Aprori关联挖掘算法的程序编写&#xff1b; &…