当前位置: 首页 > news >正文

并发编程学习篇ReentrantLock设计思想剖析

一、AQS原理剖析

什么是AQS

  1. java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如 等待队列条件队列独占获取共享获取
  2. 而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的 同步器
  3. JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的
  4. 一般是通过一个内部类Sync继承 AQS,然后将同步器所有调用都映射到Sync对应的方法

AQS具备的特性: 阻塞等待队列、共享/独占、公平/非公平、可重入、允许中断

AQS内部维护属性 “volatile int state” 表示资源的可用状态

State三种访问方式: getState() 、setState() 、compareAndSetState()

AQS定义两种资源共享方式

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

AQS定义两种队列

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

  2. 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁

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

  1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。

  2. CANCELLED,值为1,表示当前的线程被取消;

  3. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;

  4. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;

  5. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

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

自定义同步器实现时主要实现以下几种方法:

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

同步等待队列

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

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

  1. 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程

  2. 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

  3. 通过 signalsignalAll 将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)

在这里插入图片描述

条件等待队列

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

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

Condition接口详解
在这里插入图片描述

  1. 调用Condition#await方法会 释放当前持有的锁,然后阻塞当前线程,同时向Condition队列 尾部 添加一个节点,所以调用Condition#await方法的时候必须持有锁。

  2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部

  3. 然后唤醒因调condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

等待唤醒机制之await/signal测试

@Slf4j
public class ConditionTest {public static void main(String[] args) {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {lock.lock();try {log.debug(Thread.currentThread().getName() + " 开始处理任务");condition.await();log.debug(Thread.currentThread().getName() + " 结束处理任务");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();new Thread(() -> {lock.lock();try {log.debug(Thread.currentThread().getName() + " 开始处理任务");Thread.sleep(2000);condition.signal();log.debug(Thread.currentThread().getName() + " 结束处理任务");} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}).start();}

二、ReentrantLock介绍

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

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

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 与 synchronized 一样,都支持可重入

在这里插入图片描述

三、ReentrantLock使用

3.1 线程安全测试

public class ReentrantLockDemo {private static  int sum = 0;private static Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 3; i++) {Thread thread = new Thread(()->{//加锁lock.lock();try {for (int j = 0; j < 10000; j++) {sum++;}} finally {// 解锁lock.unlock();}});thread.start();}Thread.sleep(2000);System.out.println(sum);}

3.2 可重入

@Slf4j
public class ReentrantLockDemo2 {public static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {method1();}public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}}public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}}public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}}

3.3 可中断

@Slf4j
public class ReentrantLockDemo3 {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("t1启动...");try {lock.lockInterruptibly();try {log.debug("t1获得了锁");} finally {lock.unlock();}} catch (InterruptedException e) {e.printStackTrace();log.debug("t1等锁的过程中被中断");}}, "t1");lock.lock();try {log.debug("main线程获得了锁");t1.start();//先让线程t1执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}t1.interrupt();log.debug("线程t1执行中断");} finally {lock.unlock();}}

3.4 锁超时

立即失败

@Slf4j
public class ReentrantLockDemo4 {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("t1启动...");// 注意: 即使是设置的公平锁,此方法也会立即返回获取锁成功或失败,公平策略不生效if (!lock.tryLock()) {log.debug("t1获取锁失败,立即返回false");return;}try {log.debug("t1获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();try {log.debug("main线程获得了锁");t1.start();//先让线程t1执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}} finally {lock.unlock();}}

超时失败

@Slf4j
public class ReentrantLockDemo4 {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("t1启动...");//超时try {if (!lock.tryLock(1, TimeUnit.SECONDS)) {log.debug("等待 1s 后获取锁失败,返回");return;}} catch (InterruptedException e) {e.printStackTrace();return;}try {log.debug("t1获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();try {log.debug("main线程获得了锁");t1.start();//先让线程t1执行try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}} finally {lock.unlock();}}

3.5 公平锁

ReentrantLock 默认是不公平的

@Slf4j
public class ReentrantLockDemo5 {public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock(true); //公平锁 for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}log.debug(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 {log.debug(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "强行插入" + i).start();}}
}输出结果:t0 running...
t1 running...
t2 running...
......
强行插入0 running...
强行插入1 running...
强行插入2 running...
......

3.6 非公平锁

在这里插入图片描述

四、ReentrantLock源码剖析

4.1 lock方法

image.png

公平&非公平的方法源码

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

4.2 acquire方法

acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法

// 核心acquire     arg = 1
public final void acquire(int arg) {/*** 1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法,否则执行&&后面的方法* * 2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,并且插入到AQS的队列的末尾,并且作为tail* * 3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源,否则尝试将线程挂起,阻塞起来!*/if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

4.3 tryAcquire方法

tryAcquire分为公平和非公平两种

tryAcquire主要做了两件事:

  • 如果state为0,尝试获取锁资源
  • 如果state不为0,看一下是不是锁重入操作,如果不是

非公平:

// 非公平锁实现!
final boolean nonfairTryAcquire(int acquires) {// 拿到当前线程!final Thread current = Thread.currentThread();// 拿到AQS的stateint c = getState();// 如果state == 0,说明没有线程占用着当前的锁资源if (c == 0) {// 没人占用锁资源,我直接抢一波(不管有没有线程在排队)if (compareAndSetState(0, acquires)) {// 将当前占用这个互斥锁的线程属性设置为当前线程setExclusiveOwnerThread(current);// 返回true,拿锁成功return true;}}// 当前state != 0,说明有线程占用着锁资源// 判断拿着锁的线程是不是当前线程(锁重入)else if (current == getExclusiveOwnerThread()) {// 将state再次+1int nextc = c + acquires;// 锁重入是否超过最大限制// 0(符号位) 1111111 11111111 11111111 11111111   + 1// 1(符号位)0000000 00000000 00000000 00000000// 抛出errorif (nextc < 0) throw new Error("Maximum lock count exceeded");// 将值设置给statesetState(nextc);// 返回true,拿锁成功return true;}return false;
}

公平锁:

// 公平锁实现
protected final boolean tryAcquire(int acquires) {// 拿到当前线程!final Thread current = Thread.currentThread();// 拿到AQS的stateint c = getState();// c == 0 没有其他线程获取锁if (c == 0) {// 判断是否有线程在排队,如果有线程排队,直接不执行返回最外层的false// 如果没有线程排队,执行compareAndSetState()方法,CAS尝试修改status属性为1(获取锁资源)if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current); // 设置当前独占锁的线程return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

4.4 addWaiter方法

在获取锁资源 失败 后,需要将当前线程封装为Node对象,并且插入到AQS队列的末尾

// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {// 将当前线程封装为Node对象,mode为null,代表互斥锁Node node = new Node(Thread.currentThread(), mode);// pred是tail节点Node pred = tail;// 如果pred不为null,有线程正在排队if (pred != null) {// 将当前节点的prev,指定tail尾节点node.prev = pred;// 以CAS的方式,将当前节点变为tail节点if (compareAndSetTail(pred, node)) {// 之前的tail的next指向当前节点pred.next = node;return node;}}// 添加的流程为,  自己prev指向、tail指向自己、前节点next指向我// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列enq(node);return node;
}// enq,无论怎样都添加进入
private Node enq(final Node node) {for (;;) {// 拿到tailNode t = tail;// 如果tail为null,说明当前没有Node在队列中if (t == null) { // 创建一个新的Node作为head,并且将tail和head指向一个Nodeif (compareAndSetHead(new Node()))tail = head;} else {// 和上述代码一致!node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

4.5 acquireQueued

  1. acquireQueued方法会查看当前排队的Node是否是head的next
  2. 如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
  3. 在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
  • 如果为1,代表是取消的节点,不能挂起

  • 如果为-1,代表挂起当前线程

  • 如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程

// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {// 标识。boolean failed = true;try {// 循环走起for (;;) {// 拿到上一个节点final Node p = node.predecessor();if (p == head && // 说明当前节点是head的nexttryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false// 进来说明拿到锁资源成功// 将当前节点置位head,thread和prev属性置位nullsetHead(node);// 帮助快速GCp.next = null; // 设置获取锁资源成功failed = false;// 不管线程中断。return interrupted;}// 如果不是或者获取锁资源失败,尝试将线程挂起// 第一个事情,当前节点的上一个节点的状态正常!// 第二个事情,挂起线程if (shouldParkAfterFailedAcquire(p, node) &&// 通过LockSupport将当前线程挂起parkAndCheckInterrupt())}} finally {if (failed)cancelAcquire(node);}
}// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 拿到上一个节点的状态int ws = pred.waitStatus;// 如果上一个节点为 -1if (ws == Node.SIGNAL)// 返回true,挂起线程return true;// 如果上一个节点是取消状态if (ws > 0) {// 循环往前找,找到一个状态小于等于0的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将小于等于0的节点状态该为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

4.6 unlock方法

释放锁资源:

  • 将state-1。
  • 如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)

释放锁不分公平和非公平,就一个方法。

// 真正释放锁资源的方法
public final boolean release(int arg) {// 核心的释放锁资源方法if (tryRelease(arg)) {// 释放锁资源释放干净了。  (state == 0)Node h = head;// 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程if (h != null && h.waitStatus != 0)// 唤醒线程unparkSuccessor(h);return true;}// 释放锁成功,但是state != 0return false;
}// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {// 获取state - 1int c = getState() - releases;// 如果释放锁的线程不是占用锁的线程,抛异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();// 是否成功的将锁资源释放利索 (state == 0)boolean free = false;if (c == 0) {// 锁资源释放干净。free = true;// 将占用锁资源的属性设置为nullsetExclusiveOwnerThread(null);}// 将state赋值setState(c);// 返回true,代表释放干净了return free;
}// 唤醒节点
private void unparkSuccessor(Node node) {// 拿到头节点状态int ws = node.waitStatus;// 如果头节点状态小于0,换为0if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 拿到当前节点的nextNode s = node.next;// 如果s == null ,或者s的状态为1if (s == null || s.waitStatus > 0) {// next节点不需要唤醒,需要唤醒next的nexts = null;// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 经过循环的获取,如果拿到状态正常的节点,并且不为nullif (s != null)// 唤醒线程LockSupport.unpark(s.thread);
}

为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?

因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。

如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~

如果从后往前找,肯定可以找到全部的节点。

相关文章:

并发编程学习篇ReentrantLock设计思想剖析

一、AQS原理剖析 什么是AQS java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为&#xff0c;比如 等待队列、条件队列、独占获取、共享获取等而这些行为的抽象就是基于AbstractQueuedSynchronizer&#xff08;简称AQS&#xff09;实现的&#xff0c;AQS是一…...

区分效度全流程分析

基本说明 区分效度&#xff08;又称判别效度、区别效度&#xff09;&#xff0c;其实质也是一种结构效度。区分效度强调本不应该在同一因子的测量项&#xff0c;确实不在同一因子下面。比如说&#xff0c;测量项A和 B分别测量两个属性&#xff0c;应该分属于因子A和因子B中&…...

【华为OD机试模拟题】用 C++ 实现 - 找数字(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…...

从0开始写Vue项目-Vue实现用户数据批量上传和数据导出

从0开始写Vue项目-环境和项目搭建_慕言要努力的博客-CSDN博客从0开始写Vue项目-Vue2集成Element-ui和后台主体框架搭建_慕言要努力的博客-CSDN博客从0开始写Vue项目-Vue页面主体布局和登录、注册页面_慕言要努力的博客-CSDN博客从0开始写Vue项目-SpringBoot整合Mybatis-plus实现…...

企业容器云管理平台选型指南

作者简介 涂家英&#xff0c;SUSE 资深架构师&#xff0c;专注 Cloud-Native 相关产品和解决方案设计&#xff0c;在企业级云原生平台建设领域拥有丰富的经验。 数字时代下的容器云管理平台 数字时代&#xff0c;市场竞争加剧&#xff0c;业务需求日新月异&#xff0c;敏态 IT…...

OpenGL超级宝典学习笔记:着色器存储区块、原子内存操作、内存屏障

前言 本篇在讲什么 本篇为蓝宝书学习笔记 着色器存储区块 原子内存操作 内存屏障 本篇适合什么 适合初学Open的小白 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 具有全流程的图文教学 重…...

SpringMVC框架知识详解(入门版)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

25-动画和过渡

动画和过渡 一、动画 使用css动画样式&#xff0c;配合vue实现动画效果。 编写模板 <template><div><button click"isShow !isShow">显示/隐藏</button><h1 v-show"isShow">你好啊</h1></div> </templa…...

Linux 操作系统原理 — 虚拟内存管理

目录 文章目录 目录虚拟内存技术页式内存管理技术x86_32 CPU 虚拟内存虚拟地址格式与内核页表虚拟内存空间Kernel SpaceUser Spacex86_64 CPU 虚拟内存虚拟地址格式与内核页表(四级页表)虚拟内存空间TLB 缓冲(快表)进程页表虚拟内存技术 虚拟内存技术是操作系统实现的一种…...

保持超低温环境新方法:功耗降至十分之一!

&#xff08;图片来源&#xff1a;网络&#xff09;量子比特是量子计算机的主要构建部分&#xff0c;然而热量会导致量子比特容易出错&#xff0c;因此量子系统通常保存在超低温稀释制冷机内&#xff0c;可以将温度保持在绝对零度&#xff08;−273.15℃&#xff09;以上。但是…...

论文投稿指南——中文核心期刊推荐(音乐)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…...

es-10搜索推荐suggest

搜索推荐&#xff1a;Suggest 概述 搜索一般都会要求具有“搜索推荐”或者叫“搜索补全”的功能&#xff0c;即在用户输入搜索的过程中&#xff0c;进行自动补全或者纠错。以此来提高搜索文档的匹配精准度&#xff0c;进而提升用户的搜索体验&#xff0c;这就是Suggest。 四…...

VMware ESXi 7.0 Update 3k - 领先的裸机 Hypervisor (sysin Custom Image)

VMware ESXi 7.0 Update 3k - 领先的裸机 Hypervisor (sysin Custom Image) VMware ESXi 7.0 Update 3k Standard & All Custom Image for ESXi 7.0 U3k Install CD 请访问原文链接&#xff1a;https://sysin.org/blog/vmware-esxi-7-u3/&#xff0c;查看最新版。原创作品…...

JVM整体分析篇

这里写目录标题JVM的组成部分1.类装载子系统1.1一个类加载到JVM的过程1.2类加载机制1.3为什么设计双亲委派机制1.4怎么打破双亲委派机制2.运行时数据区2.1线程私有及共享2.2JVM内存区结构2.3JVM参数设置经验3.Java对象的生命周期3.1.对象的创建3.2.对象大小的计算&#xff08;6…...

【Python入门第十七天】Python While 循环

Python 循环 Python 有两个原始的循环命令&#xff1a; while 循环for 循环 while 循环 如果使用 while 循环&#xff0c;只要条件为真&#xff0c;我们就可以执行一组语句。 实例 只要 i 小于 7&#xff0c;打印 i&#xff1a; i 1 while i < 7:print(i)i 1运行实…...

怎样激发读者好奇心?短视频营销之场景化

目录 激发读者好奇心&#xff1f;四个小技巧帮你搞定 1.省略法 2.欲言又止法: 3.问句法:就是用疑问的形式引起别人的好奇。 4.反差法 选择合适的主题。 利用场景化效果 使用滤镜。 如何提高用户的留存率。 1、设置一个有趣的话题。 2、用好道具。 3、多用竖屏。 什…...

【LeetCode】剑指 Offer 14- II. 剪绳子 II p96 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/jian-sheng-zi-ii-lcof/ 1. 题目介绍&#xff08;14- II. 剪绳子 II&#xff09; 给你一根长度为 n 的绳子&#xff0c;请把绳子剪成整数长度的 m 段&#xff08;m、n都是整数&#xff0c;n>1并且m>1&#xff09;&…...

【红黑树】红黑树插入操作相关的细节和疑难拆解分析

本文就红黑树的插入操作进行细致到每一个小步骤的解析。1&#xff0c;成员变量本红黑树使用了三叉链结构&#xff0c;使用的时候尤其要记得处理指向父亲的指针。为何在节点的构造函数中&#xff0c;默认节点的颜色为红色&#xff1f;因为考虑到红黑树的性质&#xff08;对于每个…...

字符串匹配--strstr函数的模拟实现思路和代码

一&#xff0c;strstr函数 原型&#xff1a; const char * strstr ( const char * str1, const char * str2 );char * strstr ( char * str1, const char * str2 ); strstr是一个字符串匹配函数&#xff0c;在str1中去寻找str2&#xff0c;如果找到&#xff0c;返回str2在…...

【ArcGIS Pro二次开发】(7):地图(Map)的基本操作

地图是ArcGIS Pro中的基础起点&#xff0c;也是大多数工程的基础。主要用于显示表示空间数据的图层。 一、地图(Map)的基本操作示例 1、获取当前地图 var map MapView.Active.Map; 2、获取一级图层 var lys map.Layers; 用于获取地图中的单一图层&#xff0c;以及图层组…...

python 自动化测试 pytest 的使用

pytest 是一款以python为开发语言的第三方测试&#xff0c;主要特点如下&#xff1a; 比自带的 unittest 更简洁高效&#xff0c;兼容 unittest框架 支持参数化 可以更精确的控制要测试的测试用例 丰富的插件&#xff0c;已有300多个各种各样的插件&#xff0c;也可自定义扩…...

闭包(回顾)

概念作用保护作用保存作用优缺点命名空间 概念 闭包(closure)指有权访问另一个函数作用域中变量的函数 — Javacript高级程序设计 p309 简单理解&#xff0c;一个作用域可以访问另一个函数内部的私有变量 // 其中 test就是一个闭包 function fn(){var num 10function test …...

利用好这两个方法,服务型企业缺成本票不再难解决!

现代服务业属于人才密集型和技术型类别&#xff0c;其中囊括了不少技术&#xff0c;知识&#xff0c;智力服务等产业&#xff1a;信息技术&#xff0c;文化创意&#xff0c;营销策划&#xff0c;广告设计&#xff0c;以及咨询&#xff0c;商务和法律服务。 在金税三期完善之前…...

前端面试编程题(异步调度,Promise实现、占用空间大小、渲染虚拟节点、实现for of)

目录 异步调度问题 题目一 答案 题目二 答案 递归输出 题目一 答案 Promise相关 题目一 答案 占用空间大小 题目一 答案 渲染虚拟节点 题目一 答案 实现for of 题目一 答案 异步调度问题 题目一 1.实现一个带并发限制的异步调度Scheduler&#xff0c;保证同…...

复旦团队发布国内首个模型MOSS 类ChatGPT

复旦团队发布国内首个模型MOSS 类ChatGPT 首先看到这个标题&#xff0c;还有这个名字&#xff0c;我是正经&#xff08;zhen jing&#xff09;的 &#xff08;bu shi 流浪地球&#xff1f;550W&#xff1f;不了解的可以把550W倒过来写&#xff0c;就懂了 看到新闻里的一些图…...

5.35 综合案例2.0 -称重数据上传云端

综合案例2.0 - 称重数据上传云端案例说明连线功能实现1.阿里云平台连接代码应用开发3.1新建‘普通项目’3.2关联产品和设备3.3新建‘移动应用’3.4添加组件3.5配置组件信息3.6保存预览案例说明 使用hx711串口模块称重,结合IOT studio制作手机APP远程控制并采集物体重量。 hx7…...

如何让人机对话更自然?

来源&#xff1a;投稿 作者&#xff1a;顾相欢 编辑&#xff1a;学姐 AAAI-2022|定制对话的人设和知识背景 原文标题&#xff1a; Call for Customized Conversation: Customized Conversation Grounding Persona and Knowledge 原文链接&#xff1a; https://arxiv.org/ab…...

Python每日一练(20230224)

目录 1. 列表奇偶拆分 ★ 2. 二叉树的后序遍历 ★★ 3. 接雨水 ★★★ 附录 二叉树 特点 性质 特殊二叉树 满二叉树 完全二叉树 完全二叉树性质 二叉树的遍历 1. 列表奇偶拆分 【问题描述】 输入一个列表&#xff0c;包含若干个整数&#xff08;允许为空&#xff…...

【Linux】-- Shell的运行原理、Linux当中的权限

目录 Shell的运行原理 Linux权限的概念 su命令 权限 文件访问权限的相关设置方法 chmod指令 chown指令 chgrp指令 sudo命令 文件的常见问题 umask 粘滞位 关于权限的总结 Shell的运行原理 Shell运行原理 —— 外壳程序。 Linux严格意义上说的是一个操作系统&…...

MOS管选型参数:VGS(th)

MOS管选型参数&#xff1a;VGS(th) VGS&#xff08;th&#xff09;&#xff1a;开启电压&#xff08;阀值电压&#xff09;。当外加栅极控制电压 VGS 超过 VGS&#xff08;th&#xff09; 时&#xff0c;漏区和源区的表面反型层形成了连接的沟道。应用中&#xff0c;常将漏极短…...