并发编程-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,一经查实,立即删除!

相关文章

【DevOps】运维过程中经常遇到的Http错误码问题分析(二)

目录 一、HTTP 错误400 Bad Request 1、理解 400 Bad Request 错误 2、排查 400 Bad Request 错误 3、常见的解决方法 二、HTTP 错误401 Unauthorized 1、理解 401 Unauthorized 错误 2、排查 401 Unauthorized 错误 3、常见的解决方法 一、HTTP 错误400 Bad Request …

文件上传漏洞: 绕过方式及原理[表格]

序号绕过原理原理简述详细技术解释绕过方法1前端校验绕过禁用或绕过前端JavaScript验证前端JavaScript用于限制用户上传文件类型&#xff0c;但可被用户禁用或修改使用浏览器插件或开发者工具禁用JavaScript&#xff0c;或修改上传逻辑2MIME类型欺骗更改文件MIME类型以欺骗服务…

海康威视摄像头批量更改源码

更改OSD通道名称 # codingutf-8 import os import time import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import xml.etree.ElementTree as ET#和监控摄像头通讯需要一个双方认可的密钥&#xff0c;可以随机生成 def generate_key():# 生成一个16字…

LabVIEW与OpenCV图像处理对比

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

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

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

kubebuilder示例-新

生成代码 因为kubebuilder 命令工具需要依赖linux环境 所以本例方法使用git 同步代码 在window开发后提交 linux项目里拉代码运行的方式 创建go mod 项目 go mod init kubebuild_demo 初始化项目 生成基础框架 domain 生成group用的 kubebuilder init --domain example.com …

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;通过蜂鸣器进行提示。该设…

condition_variable---C++条件变量

文章目录 1.C11条件变量2. 相关api方法2.1 wait方法 2.2 wait_for 方法2.3 wait_until 方法2.4 notify_one 和 notify_all 方法 3虚假唤醒等问题 1.C11条件变量 std::condition_variable 是 C11 标准库中提供的一个类&#xff0c;用于在多线程环境中实现线程间的同步和通信。通…

CentOS7安装部署双版本MySQL

文章目录 CentOS7安装部署双版本MySQL一、前言1.场景2.环境 二、安装MySQL 5.6三、安装MySQL 8.01. 解压并移动 MySQL 文件2. 创建并配置 my.cnf3.修改mysql.server4. 添加用户组和用户5. 初始化 MySQL 数据目录6. 创建systemd服务文件7.创建符号链接8.首次登录9.允许所有主机连…

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

初中物理知识点大全 声现象知识归纳 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…

【Android】自定义换肤框架04之Skinner框架设计

到上一章为止&#xff0c;我们已经完整讲解了换肤所用到的所有技术点 这一章&#xff0c;我们来梳理下&#xff0c;如何从整体和细节上&#xff0c;进行代码设计 毕竟&#xff0c;我们不止是简单实现功能&#xff0c;要做到功能清晰&#xff0c;使用灵活 文章目录 核心组件使…

探索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…

用于实时语义分割的可重参数化双分辨率网络

文章目录 摘要一、引言II、相关工作III、方法IV. 实验V、结论摘要 https://arxiv.org/pdf/2406.12496 语义分割在自动驾驶和医学图像等应用中发挥着关键作用。尽管现有的实时语义分割模型在准确性和速度之间取得了令人称赞的平衡,但其多路径块仍然影响着整体速度。为了解决这…

洛谷 P2141 [NOIP2014 普及组] 珠心算测验

本文由Jzwalliser原创&#xff0c;发布在CSDN平台上&#xff0c;遵循CC 4.0 BY-SA协议。 因此&#xff0c;若需转载/引用本文&#xff0c;请注明作者并附原文链接&#xff0c;且禁止删除/修改本段文字。 违者必究&#xff0c;谢谢配合。 个人主页&#xff1a;blog.csdn.net/jzw…