并发编程-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.rhkb.cn/news/371607.html

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

相关文章

JavaWeb—js(2)

函数 /* 创建一个方法形参不需要写参数类型&#xff0c;多个参数直接用逗号隔开就可以 */function show(a,b,c){console.log(我是show方法,a,b,c)return ab;}// 调用方法//调用方法时 形参个数如果和实参个数对不上不会有异常show();show(你好)show(1,2);let add show(1,2,3…

君方智能设计平台-事务管理技术方案

1.背景介绍 事务处理是指对数据进行一组操作&#xff0c;这些操作要么全部成功&#xff0c;要么全部失败&#xff0c;以确保数据的一致性和完整性。软件的事务管理主要实现方案主要涉及以下几个方面&#xff1a; &#xff08;1&#xff09;数据一致性&#xff1a;在CAD软件中…

JCR一区 | Matlab实现GAF-PCNN-MATT、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断

JJCR一区 | Matlab实现GAF-PCNN-MATT、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断 目录 JJCR一区 | Matlab实现GAF-PCNN-MATT、GASF-CNN、GADF-CNN的多特征输入数据分类预测/故障诊断分类效果格拉姆矩阵图GAF-PCNN-MATTGASF-CNNGADF-CNN 基本介绍程序设计参考资料 分…

npm 淘宝镜像证书过期,错误信息 Could not retrieve https://npm.taobao.org/mirrors/node/latest

更换 npm 证书 问题描述报错原因更换步骤1 找到 nvm 安装目录2 发现证书过期3 更换新地址4 保存后&#xff0c;重新安装成功 问题描述 在使用 nvm 安装新版本时&#xff0c;未成功&#xff0c;出现报错&#xff1a; Could not retrieve https://npm.taobao.org/mirrors/node/l…

Python酷库之旅-第三方库Pandas(009)

目录 一、用法精讲 19、pandas.read_xml函数 19-1、语法 19-2、参数 19-3、功能 19-4、返回值 19-5、说明 19-6、用法 19-6-1、数据准备 19-6-2、代码示例 19-6-3、结果输出 20、pandas.DataFrame.to_xml函数 20-1、语法 20-2、参数 20-3、功能 20-4、返回值 …

AI Earth ——开发者模式案例10:基于 CNN 的 AI 分类模型开发

基于 CNN 的 AI 分类模型开发 本案例主要介绍如何快速利用 AIE Python SDK 创建机器学习建模流程。我们主要使用到 Python SDK的Machine Learning Proxy 模块(下文简称 AieMlProxy )。该模块涵盖了一系列用户与训练集群之间的交互接口,包括:鉴权、数据加载、训练任务提交、…

Shell Expect自动化交互(示例)

Shell Expect自动化交互 日常linux运维时&#xff0c;经常需要远程登录到服务器&#xff0c;登录过程中需要交互的过程&#xff0c;可能需要输入yes/no等信息&#xff0c;所以就用到expect来实现交互。 关键语法 ❶&#xff3b;#!/usr/bin/expect&#xff3d; 这一行告诉操…

MySQL之备份与恢复和MySQL用户工具(一)

备份与恢复 备份脚本化 为备份写一些脚本是标准做法。展示一个示例程序&#xff0c;其中必定有很多辅助内容&#xff0c;这只会增加篇幅&#xff0c;在这里我们更愿意列举一些典型的备份脚本功能&#xff0c;展示一些Perl脚本的代码片段。你可以把这些当作可重用的代码块&…

io流 多线程

目录 一、io流 1.什么是io流 2.流的方向 i.输入流 ii.输出流 3.操作文件的类型 i.字节流 1.拷贝 ii.字符流 ​3.字符流输出流出数据 4.字节流和字符流的使用场景 5.练习 6.缓冲流 1.字节缓冲流拷贝文件 2.字符缓冲流特有的方法 1.方法 2.总结 7.转换流基本用法…

数字信号处理及MATLAB仿真(3)——量化的其他概念

上回书说到AD转换的两个步骤——量化与采样两个步骤。现在更加深入的去了解以下对应的概念。学无止境&#xff0c;要不断地努力才有好的收获。万丈高楼平地起&#xff0c;唯有打好基础&#xff0c;才能踏实前行。 不说了&#xff0c;今天咱们继续说说这两个步骤&#xff0c;首先…

【国产开源可视化引擎Meta2d.js】网格

画布背景网格 在线体验&#xff1a; 乐吾乐2D可视化 示例&#xff1a; // 设置默认缺省网格属性 meta2d.store.options.grid true; // 开启 meta2d.store.options.gridColor eeeeee; // 网格线条颜色 meta2d.store.options.gridSize 10; // 格子大小// 设置单个图纸的网格…

pnpm的坑

请问pnpm的两个坑怎么解决&#xff1a; 第一个坑&#xff1a;没有节省磁盘空间 我已经配置了依赖的存储位置&#xff0c; 但我在项目里pnpm install以后&#xff0c;发现依赖包还是很大&#xff0c; 然后发现里面的链接并不是指向先前配置的依赖存储位置&#xff0c;而是指…

java核心-泛型

目录 概述什么是泛型分类泛型类泛型接口泛型方法 泛型通配符分类 泛型类型擦除分类无限制类型擦除有限制类型擦除 问题需求第一种第二种 概述 了解泛型有利于学习 jdk 、中间件的源码&#xff0c;提升代码抽象能力&#xff0c;封装通用性更强的组件。 什么是泛型 在定义类、接…

VSCode设置好看清晰的字体!中文用鸿蒙,英文用Jetbrains Mono

一、中文字体——HarmonyOS Sans SC 1、下载字体 官网地址&#xff1a;https://developer.huawei.com/consumer/cn/design/resource/ 直接下载&#xff1a;https://communityfile-drcn.op.dbankcloud.cn/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20230517…

加装德国进口高精度主轴 智能手机壳「高质量高效率」钻孔铣槽

在当前高度智能化的社会背景下&#xff0c;智能手机早已成为人们生活、工作的必备品&#xff0c;智能手机壳作市场需求量巨大。智能手机壳的加工过程涉及多个环节&#xff0c;包括钻孔和铣槽等。钻孔要求精度高、孔位准确&#xff0c;而铣槽则需要保证槽位规整、深度适宜。这些…

【漏洞复现】TerraMaster TOS exportUser.php 远程命令执行

免责声明&#xff1a; 本文内容旨在提供有关特定漏洞或安全漏洞的信息&#xff0c;以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步&#xff0c;并非出于任何恶意目的。阅读者应该明白&#xff0c;在利用本文提到的漏洞信息或进行相关测…

Leetcode3195. 包含所有 1 的最小矩形面积 I

Every day a Leetcode 题目来源&#xff1a;3195. 包含所有 1 的最小矩形面积 I 解法1&#xff1a;遍历 设最左、最右、最上、最下的 1 的行号/列号分别为 left、right、top、bottom&#xff0c;则答案为&#xff1a;(right - left 1) * (bottom - top 1)。 代码&#xf…

JAVA 对象存储OSS工具类(腾讯云)

对象存储OSS工具类 import com.qcloud.cos.COSClient; import com.qcloud.cos.ClientConfig; import com.qcloud.cos.auth.BasicCOSCredentials; import com.qcloud.cos.auth.COSCredentials; import com.qcloud.cos.model.ObjectMetadata; import com.qcloud.cos.model.PutObj…

基于java+springboot+vue实现的仓库管理系统(文末源码+lw+ppt)23-499

第1章 绪论 伴随着信息社会的飞速发展&#xff0c;仓库管理所面临的问题也一个接一个的出现&#xff0c;所以现在最该解决的问题就是信息的实时查询和访问需求的问题&#xff0c;以及如何利用快捷便利的方式让访问者在广大信息系统中进行查询、分享、储存和管理。这对我们的现…

WordPress:快速搭建站点,wp安装及模版介绍

最近搭建个人站点比较多&#xff0c;都是想把业务做到国外&#xff0c;通过google来引流&#xff0c;那我们今年就来介绍一个比较受欢迎的站点平台wordPress。WordPress是使用PHP语言开发的博客平台&#xff0c;用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也…