Java AQS

news2024/9/24 19:18:47

AQS 是什么

  • AQS 的全称为 AbstractQueuedSynchronizer,翻译过来的意思就是抽象队列同步器,这个类在 java.util.concurrent.locks 包下面
  • Java 中的大部分同步类(Lock、Semaphore、ReentrantLock等) 都是基于 AQS 实现的
  • AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
  • AQS 就是一个抽象类,主要用来构建锁和同步器
    public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { }

AQS 有哪些变量

// 队列头节点
private transient volatile Node head;

// 队列尾节点
private transient volatile Node tail;

// 队列状态
private volatile int state;

// 持有锁的线程 (继承 AbstractOwnableSynchronizer 操作该变量)
private transient Thread exclusiveOwnerThread;

AQS 底层数据结构

  • state:初始值为 0,如果有线程持有变量为 1,如果是可重入锁,同个线程可以继续叠加
  • CLH(Craig Landin Hagersten)队列:是一个虚拟的双向队列、FIFO 队列,虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系,AQS 将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配
  • 为什么使用双向而不用单向 ?
    • 假设队列是单向的如:head -> n1 -> n2 -> tail,出队的时候获取 n1 很简单,head.next 就行了
    • 入队就麻烦了,因为是一个 FIFO 队列,后放的元素要放在尾部,要遍历整个链表到 n2,然后 n2.next = n3;n3.next = tail,入队的复杂度就是 O(n) ,而且 tail 也失去他的意义
    • 相反双向链表出队和入队都是 O(1) 时间复杂度,说白了空间换时间

  • condition 队列,只有使用到 Condition 对象才有的队列
  • 单向队列,等待的线程就存在该队列中

AQS 扩展组件

线程池

  • 创建一个线程池,模拟请求并发
import java.util.concurrent.*;

public class ThreadPool {

    private static final RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

    private static final int MAX_QUEUE_SIZE = 200;

    // 初始化线程池 (参数可以根据实际情况调整)
    public static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
            20,
            40,
            0,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(MAX_QUEUE_SIZE),
            Executors.defaultThreadFactory(),
            handler
    );

    // 停止线程池
    public static void shutdown() {
        POOL.shutdown();
    }
}

使用的日志依赖

<dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-simple</artifactId>
   <version>1.7.25</version>
</dependency>

CountDownLatch

  • 等待一组线程执行完成,再执行下一步操作
  • 例如:服务有多个地区,但是账号是全局唯一的,在用户注册的时候需要遍历每个区域是否存在账号,全部区域都判断账号不存在,再进行下一步,这时候就可以使用 CountDownLatch 同时校验每个区域是否存在账号
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * CountDownLatch:
 * 等待多个线程执行完成才进行下一步操作
 */
public class CountDownLatchExample {

    private static final Logger log = LoggerFactory.getLogger(CountDownLatchExample.class);

    private static final int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {

        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            ThreadPool.POOL.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception ", e);
                } finally {
                    // CountDownLatch 计数器减 1
                    countDownLatch.countDown();
                }
            });
        }

        // 等待线程执行完成
        // countDownLatch.await();

        // 超过等待时间就不等待
        countDownLatch.await(10, TimeUnit.MILLISECONDS);

        log.info("finish");

        ThreadPool.shutdown();

    }

    public static void test(int threadNum) throws Exception {
        Thread.sleep(100L);
        log.info("{}", threadNum);
        Thread.sleep(100L);
    }

}

Semaphore

  • 信号量:可以指定某个共享资源可以同时最多被几个线程访问
  • 应用场景:数据库的连接,数据库同时最多可以被几个线程访问,通过 Semaphore 机制,可以限制最大的访问线程数
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * Semaphore 控制并发线程的访问量
 * 控制某个资源可被线程访问的个数
 */
public class SemaphoreExample {

    private static final Logger log = LoggerFactory.getLogger(SemaphoreExample.class);

    private static final int threadCount = 20;

    public static void main(String[] args) throws Exception {

        // 初始化 Semaphore,有3个令牌可以获取
        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            ThreadPool.POOL.execute(() -> {
                try {

                    // 获取许可之后再执行任务,任务执行完成释放许可

                    //semaphore.acquire(); // 获取一个许可
                    //semaphore.release(); // 释放一个许可

                    //semaphore.acquire(3);  // 一次获取多个许可
                    //semaphore.release(3);   // 一次释放多个许可

                    //if(semaphore.tryAcquire()) {    // 尝试获取许可,获取不到直接丢掉任务
                    //    test(threadNum);
                    //    semaphore.release();        // 释放许可
                    //}

                    //if(semaphore.tryAcquire(500, TimeUnit.MILLISECONDS)) {     // 尝试获取一个许可,超过等待时间直接丢掉任务
                    //    test(threadNum);
                    //    semaphore.release();           // 释放许可
                    //}

                    if (semaphore.tryAcquire(3, 500, TimeUnit.MILLISECONDS)) {        // 尝试获取多个许可,超过等待时间直接丢掉任务
                        test(threadNum);
                        semaphore.release(3);   // 释放多个许可
                    }

                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        ThreadPool.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(5000);
    }

}

CyclicBarrier

  • 循环屏障,当线程统一到达某个状态,再执行下一步操作 (这里和 CountDownLatch 一样)
  • 当第一次的使用结束之后,还可以再使用执行下一步操作,循环使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {

    private static final Logger log = LoggerFactory.getLogger(CyclicBarrierExample.class);

    //private static final CyclicBarrier barrier = new CyclicBarrier(5);

    // 到达屏障设置回调
    private static final CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        log.info("callback is running");
    });

    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000L);
            ThreadPool.POOL.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }

        ThreadPool.POOL.shutdown();
    }


    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        // 没有等待时间
        barrier.await();

        // 设置等待时间
        /*try {
            barrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.warn("BarrierException", e);
        }*/
        log.info("{} continue", threadNum);
    }
}

ReentrantLock

  • 可以通过构造方法执行公平锁还是非公平锁,默认非公平锁
  • 使用锁之后,一般都要在 finally 中释放锁
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {

    private static final Logger log = LoggerFactory.getLogger(ReentrantLockExample.class);

    /**
     * 可以指定公平还是非公平锁
     * 提供 Condition 类,可以分组唤醒需要唤醒的线程
     * 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()
     */
    private final Lock lock = new ReentrantLock();

    // 共享变量
    private Integer i = 0;

    public static void main(String[] args) {

        ReentrantLockExample main = new ReentrantLockExample();

        for (int i = 0; i < 100; i++) {
            ThreadPool.POOL.execute(main::testLock);
        }

        ThreadPool.POOL.shutdown();
    }

    /**
     * 测试锁
     */
    public void testLock() {
        // 获取锁
        lock.lock();
        try {
            i++;
            log.info("i 的值:" + i);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

Condition

  • Condition 对象,可以结合 ReentrantLock 一起使用
  • 通过 condition 的单向队列维护等待的线程,在获取锁之后,调用 await() 方法让线程中从 AQS 队列中移除,然后加入到 condition 队列进行等待,当收到线程的通知之后,线程再回到 AQS 队列中执行
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionalExample {

    private static final Logger log = LoggerFactory.getLogger(ConditionalExample.class);

    public static void main(String[] args) {

        ReentrantLock reentrantLock = new ReentrantLock();

        Condition condition = reentrantLock.newCondition();

        new Thread(() -> {
            try {
                // 一开始获取到锁
                reentrantLock.lock();
                log.info("wait signal"); // 1
                // 将线程从 aqs 队列中移除,加入到 condition 的等待队列
                // 等待线程的一个信号,再往下执行
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("get signal"); // 4
            reentrantLock.unlock();
        }).start();

        new Thread(() -> {
            // 由于调用 await() 方法,拿到线程的锁
            reentrantLock.lock();
            log.info("get lock"); // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 发送信息,让 condition 中的等待线程加入到 aqs 中的线程
            // 这时候线程没有被唤醒,只是把 condition 中的队列放到 aqs 中的队列
            condition.signalAll();
            log.info("send signal ~ "); // 3
            // 当前线程释放掉锁,aqs 中还存在上面线程的锁,就被唤醒了
            reentrantLock.unlock();
        }).start();
    }
}

ReentrantReadWriteLock

  • 有读写锁的锁,读锁可以重入
  • 需要留意如果,如果读取比较频繁,那么写锁会一直在等待
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockExample {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}

StampedLock

  • 可以通过悲观锁或者客观锁实现并发控制,乐观锁性能较好
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {

    private static final Logger log = LoggerFactory.getLogger(StampedLockExample.class);

    // 请求总数
    public static int clientTotal = 50;

    // 同时并发执行的线程数
    public static int threadTotal = 20;

    public static int count = 0;

    private static final StampedLock lock = new StampedLock();

    public static void main(String[] args) throws Exception {
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            ThreadPool.POOL.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        ThreadPool.POOL.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        long stamp = lock.writeLock();
        try {
            count++;
        } finally {
            lock.unlock(stamp);
        }
    }

    class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();

        void move(double deltaX, double deltaY) { // an exclusively locked method
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }

        //下面看看乐观读锁案例
        double distanceFromOrigin() { // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }

        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
                while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) { //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else { //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }

}

基于 AQS 实现同步组件

  • 定义内部类继承 AQS
  • 重写 lock() unlock() tryAcquire() tryRelease() isHeldExclusively() 方法
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class ExampleLock {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);
        }

        @Override
        protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    private Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

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

}

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExampleMain {

    private static final Logger log = LoggerFactory.getLogger(ExampleMain.class);

    static int count = 0;

    static ExampleLock lock = new ExampleLock();

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = () -> {
            try {
                lock.lock();
                for (int i = 0; i < 10000; i++) {
                    count++;
                    log.info("cur count: {}", count);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        log.info("count: {}", count);
    }
}


参考地址

  • 基于AQS 实现同步组件

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

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

相关文章

SpringBoot - 整合WebSocket时@ServerEndpoint修饰的类属性注入为null问题

SpringBoot - 整合WebSocket时ServerEndpoint修饰的类属性注入为null问题前言一. 问题复现1.1 原因分析二. 问题解决前言 最近在做一个直播弹幕系统&#xff0c;前期准备先用WebSocket来试试水。我们都知道&#xff0c;使用WebSocket只需要给对应的类加上注解ServerEndpoint即…

Linux之定时任务--crontab命令解析学习

Corntab定时任务学习 一、crond服务 在学习crontab&#xff0c;命令之前&#xff0c;我觉得有必要学习了解一下crond服务&#xff0c;因为要在linux系统下使用crontab命令需要crond的支持。Crond是Linux下要用来周期执行某种任务或者等待处理某些事件的一个守护进程。和Windo…

项目——员工管理系统

开发环境&#xff1a;vmware ubuntu18.04 实现功能&#xff1a;基本功能包括管理者和普通员工用户的登录&#xff0c;管理者拥有操作所有员工信息的最高权限&#xff0c;可以进行增删改 查等操作&#xff0c;普通用户仅拥有查看、修改个人部分信息的权限 具体功能详解&…

python 学习笔记

解决执行python"ImportError: No module named requests"问题 #切换到python的安装目录执行如下命令 D:\Python27>pip install requestsImportError: No module named bs4错误解决方法 运行脚本时提示ImportError: No module named bs4错误&#xff0c;原因&…

[附源码]计算机毕业设计基于vue的软件谷公共信息平台Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Python——有限状态机

有限状态机&#xff08;Finite-state machine, FSM&#xff09;&#xff0c;又称有限状态自动机&#xff0c;简称状态机&#xff0c;是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。FSM是一种算法思想&#xff0c;简单而言&#xff0c;有限状态机由一组状态…

【Lilishop商城】No3-5.模块详细设计,商品模块-1(商品分类、品牌管理、规格管理、参数、计量单位、店铺分类)的详细设计

仅涉及后端&#xff0c;全部目录看顶部专栏&#xff0c;代码、文档、接口路径在&#xff1a; 【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑&#xff0c;其中重点包括接口类、业务类&#xff0c;具体的结合源代码…

人脸特征点检测入门

基础 人脸特征点可以用来做脸型、眼睛形状、鼻子形状等分析&#xff0c;从而对人脸的特定位置进行修饰加工&#xff0c;实现人脸的特效美颜。人脸识别等算法可以通过对人脸的姿态进行对齐从而提高模型的精度。 68点标注是现今最通用的一种标注方案&#xff0c;早期在1999年的…

Effective C++条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining)

Effective C条款30&#xff1a;透彻了解inlining的里里外外&#xff08;Understand the ins and outs of inlining&#xff09;条款30&#xff1a;透彻了解inlining的里里外外1、inline函数的优缺点2、隐式内联和显式内联2.1 隐式内联2.2 显式内联3、函数模板必须inline么&…

自动驾驶之3D点云聚类算法调研

1. 方法 总共分为4类 基于欧式距离的聚类Supervoxel 聚类深度(Depth) 聚类Scanline Run 聚类 1.1 基于欧氏距离的聚类 思路: 在点云上构造kd-tree, 然后在某个半径阈值(例如0.5m), 则分割为一个实例。 相似算法: RBNN (radially bounded nearest neighbor graph), 2008. …

在 Ubuntu 上安装 Discourse 开发环境

本指南只针对 Discourse 开发环境的配置&#xff0c;如果你需要在生产环境中安装 Discourse &#xff0c;请访问页面&#xff1a;Install Discourse in production with the official, supported instructions - sysadmin - Discourse Meta 中的内容。 有关开发环境的设置英文原…

[Java EE初阶] 进程调度的基本过程

纪念Java EE初阶开篇文章,不放弃,不摆烂,踏平所有障碍吧!少年!奥利给!(操作系统这方面的所有文章均不作为操作系统的专业课知识学习) 文章目录1. 进程的概念2. PCB --- 进程控制块3. 并发与并行4. 进程调度的相关属性5. 内存管理总结1. 进程的概念 进程,就是跑起来的程序,我们…

【学习笔记】《Python深度学习》第七章:高级的深度学习最佳实践

文章目录1 Keras 函数式 API1.1 函数式 API 简介1.2 多输入模型1.3 多输出模型1.4 层组成的有向无环图1.5 共享层权重1.6 将模型作为层2 使用 Keras 回调函数 和 TensorBoard 检查并监控深度学习模型2.1 训练过程中将回调函数作用于模型2.2 TensorBoard 简介&#xff1a;Tensor…

【Lilishop商城】No3-4.模块详细设计,店铺店员(店铺店员、店铺部门、店铺角色)的详细设计

仅涉及后端&#xff0c;全部目录看顶部专栏&#xff0c;代码、文档、接口路径在&#xff1a; 【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑&#xff0c;其中重点包括接口类、业务类&#xff0c;具体的结合源代码…

exfat文件系统

DBR&#xff1a; DBR偏移量 字段长度&#xff08;字节&#xff09; 说明 0x40 - 0x47 8 分区的起始扇区号&#xff08;隐藏扇区数&#xff09; 0x48 - 0x4F 8 分区总扇区数 0x50 - 0x53 4 FAT表起始扇区号&#xff08;从DBR到FAT表的扇区个数&#xff09; 0x54 - 0x57 4…

【Redis】持久化操作

一、RDB(Redis Database) 1、持久化 redis一般是将数据写到内存中&#xff0c;但也可以将数据写到磁盘中&#xff0c;这个过程称之为持久化 2、什么是RDB 在指定的时间间隔内将内存中的数据集快照写入磁盘中 3、RDB是如何执行备份操作的 redis会单独创建(fork)一个子进程进行…

FPGA 20个例程篇:18.SD卡存放音频WAV播放(下)

第七章 实战项目提升&#xff0c;完善简历 18.SD卡存放音频WAV播放&#xff08;下&#xff09; 进一步地我们再结合图1的示意图来分析wav_play模块的时序逻辑设计&#xff0c;大家可以清楚地看到WM8731在Right justified和主从时钟模式下&#xff0c;是先发左声道后发右声道数…

【LeetCode】专题一 二叉树层序遍历

二叉树层序遍历 在本文中&#xff0c;我将会选取LeetCode上二叉树层序遍历的多道例题&#xff0c;并给出解答&#xff0c;通过多道题我们就可以发现&#xff0c;二叉树的层序遍历并不复杂&#xff0c;并且有着共通点。 102. 二叉树的层序遍历 给你二叉树的根节点 root &…

【Labivew】简易计算器

&#x1f6a9;write in front&#x1f6a9; &#x1f50e;大家好&#xff0c;我是謓泽&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f3c5;2021年度博客之星物联网与嵌入式开发TOP5&#xff5…

Secure CRT远程连接很快断线问题

问题描述 我们使用Secure CRT连接远程主机时可能会遇到几分钟没操作就无法操作了&#xff0c;需要断开重新连接&#xff0c;非常的麻烦&#xff0c;假如客户端或者服务端能够在快要超时的时候给对方发送一个心跳&#xff0c;得到对方响应就重置下超时时间&#xff0c;这样就能…