线程池的构造以及相关方法

news2024/11/22 22:51:11

手写线程池

package cn.itcast.n8;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.log.LogDelegateFactory;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.TestPool")
public class TestPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, 
                (queue, task)->{
            // 1. 死等
//            queue.put(task);
            // 2) 带超时等待
//            queue.offer(task, 1500, TimeUnit.MILLISECONDS);
            // 3) 让调用者放弃任务执行
//            log.debug("放弃{}", task);
            // 4) 让调用者抛出异常
//            throw new RuntimeException("任务执行失败 " + task);
            // 5) 让调用者自己执行任务
            task.run();
	        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue, T task);
}

@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

    // 获取任务时的超时时间
    private long timeout;

    private TimeUnit timeUnit;

    private RejectPolicy<Runnable> rejectPolicy;

    // 执行任务
    public void execute(Runnable task) {
        // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
        // 如果任务数超过 coreSize 时,加入任务队列暂存
        synchronized (workers) {
            if(workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker{}, {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
//                taskQueue.put(task);
                // 1) 死等
                // 2) 带超时等待
                // 3) 让调用者放弃任务执行
                // 4) 让调用者抛出异常
                // 5) 让调用者自己执行任务
                taskQueue.tryPut(rejectPolicy, task);
            }
        }
    }

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread{
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 执行任务
            // 1) 当 task 不为空,执行任务
            // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
//            while(task != null || (task = taskQueue.take()) != null) {
            while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.debug("worker 被移除{}", this);
                workers.remove(this);
            }
        }
    }
}
@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {
    // 1. 任务队列
    private Deque<T> queue = new ArrayDeque<>();

    // 2. 锁
    private ReentrantLock lock = new ReentrantLock();

    // 3. 生产者条件变量
    private Condition fullWaitSet = lock.newCondition();

    // 4. 消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();

    // 5. 容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            // 将 timeout 统一转换为 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                try {
                    // 返回值是剩余时间
                    if (nanos <= 0) {
                        return null;
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞获取
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    log.debug("等待加入任务队列 {} ...", task);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity) {
                try {
                    if(nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            // 判断队列是否满
            if(queue.size() == capcity) {
                rejectPolicy.reject(this, task);
            } else {  // 有空闲
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

ThreadPoolExecutor

image.png

线程池状态

image.png

构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

工作方式

image.png

image.png

拒绝策略

见上面
image.png

Excutors

newCachedThreadPool

作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程.
特点:
• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
• 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
• 当线程池中,没有可用线程,会重新创建一个线程
场景: 适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较 短,任务多的场景

image.png

newFixedThreadPool

作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这 些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线 程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中 等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线 程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池 中的线程将一直存在。
特征:
• 线程池中的线程处于一定的量,可以很好的控制线程的并发量
• 线程可以重复被使用,在显示关闭之前,都将一直存在
• 超出一定量的线程被提交时候需在队列中等待

场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严 格限制的场景

image.png

image.png

newSingleThreadExecutor

作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该 线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程, 那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各 个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即 可使用其他的线程。

特征: 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此 执行
场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个 线程的场景

image.png

相关方法

提交任务

    // 执行任务
    void execute(Runnable command);
    // 提交任务 task,用返回值 Future 获得任务执行结果
    <T> Future<T> submit(Callable<T> task);
    // 提交 tasks 中所有任务
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
            throws InterruptedException;
    // 提交 tasks 中所有任务,带超时时间,如果不能在规定时间内完成,就把后续的任务取消掉
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
            throws InterruptedException;
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
            throws InterruptedException, ExecutionException;
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
        long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
package cn.itcast.n8;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Slf4j(topic = "c.TestSubmit")
public class TestSubmit {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);

    }

    private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException {
        String result = pool.invokeAny(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(1000);
                    log.debug("end 1");
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(500);
                    log.debug("end 2");
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    log.debug("end 3");
                    return "3";
                }
        ));
        log.debug("{}", result);
    }

    private static void method2(ExecutorService pool) throws InterruptedException {
        List<Future<String>> futures = pool.invokeAll(Arrays.asList(
                () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "1";
                },
                () -> {
                    log.debug("begin");
                    Thread.sleep(500);
                    return "2";
                },
                () -> {
                    log.debug("begin");
                    Thread.sleep(2000);
                    return "3";
                }
        ));

        futures.forEach( f ->  {
            try {
                log.debug("{}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
    }

    private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
        Future<String> future = pool.submit(() -> {
            log.debug("running");
            Thread.sleep(1000);
            return "ok";
        });

        log.debug("{}", future.get());
    }
}

关闭线程池

shutdown

/*  
线程池状态变为 SHUTDOWN- 不会接收新任务  
- 但已提交任务会执行完  
- 此方法不会阻塞调用线程的执行  
*/  
void shutdown();  
public void shutdown() {  
    final ReentrantLock mainLock = this.mainLock;  
    mainLock.lock();  
    try {  
        checkShutdownAccess();  
        // 修改线程池状态  
        advanceRunState(SHUTDOWN);  
        // 仅会打断空闲线程  
        interruptIdleWorkers();  
        onShutdown(); // 扩展点 ScheduledThreadPoolExecutor   
         } finally {  
        mainLock.unlock();  
    }  
    // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)  
    tryTerminate();
}
shutdownnow

    /*
    线程池状态变为 STOP
    - 不会接收新任务
    - 会将队列中的任务返回
    - 并用 interrupt 的方式中断正在执行的任务
    */
    List<Runnable> shutdownNow();
    public List<Runnable> shutdownNow() {

        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 修改线程池状态
            advanceRunState(STOP);
            // 打断所有线程
            interruptWorkers();
            // 获取队列中剩余任务
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        // 尝试终结
        tryTerminate();
        return tasks;
    }

其他方法

image.png

package cn.itcast.n8;

import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.concurrent.*;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestShutDown")
public class TestShutDown {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> result1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finish...");
            return 1;
        });

        Future<Integer> result2 = pool.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finish...");
            return 2;
        });

        Future<Integer> result3 = pool.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finish...");
            return 3;
        });

        log.debug("shutdown");
//        pool.shutdown();
//        pool.awaitTermination(3, TimeUnit.SECONDS);
        List<Runnable> runnables = pool.shutdownNow();
        log.debug("other.... {}" , runnables);
    }
}

创建多大的线程池

image.png

任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。

延时执行任务

    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);  
// 添加两个任务,希望它们都在 1s 后执行  
executor.schedule(() -> {  
        System.out.println("任务1,执行时间:" + new Date());  
        try { Thread.sleep(2000); } catch (InterruptedException e) { }  
        }, 1000, TimeUnit.MILLISECONDS);  
        executor.schedule(() -> {  
        System.out.println("任务2,执行时间:" + new Date());  
        }, 1000, TimeUnit.MILLISECONDS);

定时执行任务

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); 
log.debug("start...");
pool.scheduleAtFixedRate(() -> { log.debug("running..."); }, 1, 1, TimeUnit.SECONDS);



ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); 
log.debug("start..."); 
pool.scheduleWithFixedDelay(()-> { log.debug("running..."); sleep(2); }, 1, 1, TimeUnit.SECONDS);

异常处理

方法1: 捉住异常
线程池中任务发生异常不会打印,因此需要我们自己try/catch住异常,并做打印等处理

ExecutorService pool = Executors.newFixedThreadPool(1); pool.submit(() -> { 
try { log.debug("task1"); 
int i = 1 / 0; } 
catch (Exception e)
{ log.error("error:", e); } });

方法2:使用Future
如果任务发生异常,就会把异常的信息返回回来

ExecutorService pool = Executors.newFixedThreadPool(1); Future f = pool.submit(() -> { 
log.debug("task1");
int i = 1 / 0; 
return true; }); 
log.debug("result:{}", f.get());

参考资料

  • Java线程池实现原理及其在美团业务中的实践 (lianglianglee.com)

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

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

相关文章

【卡码网C++基础课 6.数组的倒序与隔位输出】

目录 题目描述与分析一、数组二、Vector三、倒序输出数组四、隔位输出五、完整代码 题目描述与分析 题目描述&#xff1a; 给定一个整数数组&#xff0c;编写一个程序实现以下功能&#xff1a; 1.将输入的整数数组倒序输出&#xff0c;每个数之间用空格分隔。 2.从正序数组中&…

可视化大屏-实现自动滚动

一、背景&#xff1a;可视化大屏通常需要用到自动滚动的效果&#xff0c;本文主要采用的是vue-seamless-scroll组件来实现&#xff08;可参考官方文档&#xff09; 二、实现效果&#xff1a; 自动滚动 三、代码实现&#xff1a; 解题思路&#xff1a; 1.先安装依赖包 npm inst…

zTree 异步加载实现 点击加载

效果图如下 每级最多显示5000条数据&#xff0c;点击加载 会再次加载5000条数据 可以监听滚动条 动态加载 我这没实现这种方式。 <!-- ztreejs --> <script src".ztree/js/jquery.ztree.core.min.js" type"text/javascript"></script>…

DLAFormer:微软提出多任务统一的端到端文本分析Transformer模型 | ICDAR 2024

论文提出新颖的基于Transformer的端到端方法DLAFormer&#xff0c;在统一的模型中集成多个文档布局分析任务&#xff0c;包括图形页面对象检测、文本区域检测、逻辑角色分类和阅读顺序预测。为了实现这一目标&#xff0c;将各种DLA子任务视为关系预测问题并提出了统一标签空间方…

kafka监控工具EFAK

kafka监控工具&#xff08;EFAK&#xff09; 1、下载2、解压3、配置3.1、安装数据库&#xff0c;需要是mysql&#xff0c;并创建ke数据库3.2、修改配置文件 4、启动4.1、启动zookeeper4.2、启动kafka4.3、启动EFAK 5、访问http://ip:8048 github地址&#xff1a;https://github…

colmap的几种相机类型和内外参取得方法

colmap的相机类型可以参考models.h文件。 主要有以下几种相机类型&#xff1a; SimplePinhole&#xff1a; 内参格式&#xff1a;f, cx, cy 实际用的时候&#xff1a;fxfyf Pinhole: 内参格式&#xff1a;fx, fy, cx, cy 其他可以自行查看models.h文件。 内参存放在images.b…

《微信小程序开发》系列:第1篇—微信小程序简介

1.什么是微信小程序 微信小程序是一种运行在微信内部的 轻量级 应用程序。 在使用小程序时 不需要下载安装&#xff0c;用户 扫一扫 或 搜一下 即可打开应用。它也体现了 “用完即走” 的理念&#xff0c;用户不用关心安装太多应用的问题。它实现了应用“触手可及”的梦想&…

回归分析系列14— 多项式回归

17 多项式回归 17.1 简介 多项式回归是线性回归的一种扩展&#xff0c;它允许回归模型包括输入变量的高次项。这种方法特别适合处理非线性关系的数据。 17.2 多项式回归模型 在多项式回归中&#xff0c;模型形式如下&#xff1a; 其中&#xff0c;p 是多项式的阶数&#xf…

Vue项目-三级联动的路由跳转与传参

三级联动组件的路由的跳转与传参 三级联动&#xff0c;用户可以点击的&#xff1a;一级分类、二级分类和三级分类 以商城项目为例&#xff0c;Home模块跳转到Search模块&#xff0c;以及会把用户选中的产品&#xff08;产品名字、产品ID&#xff09;在路由跳转的时候&#xff…

【控糖有道,健康无忧!糖尿病患者的饮食黄金法则大揭秘】

在快节奏的现代生活中&#xff0c;糖尿病作为一种常见的慢性疾病&#xff0c;正悄然影响着越来越多人的生活质量。面对这一挑战&#xff0c;科学合理的饮食管理成为了控制病情、提升生活品质的关键。今天&#xff0c;就让我们一同揭开糖尿病饮食的黄金法则&#xff0c;让“控糖…

CentOS服务器三级等保加固

1.密码周期: vim /etc/login.defs max_days:90 mindays:2 minlen:8 warnage:72.密码复杂度: vim /etc/pam.d/system-auth &#xff1a; password requisite pam_cracklib.so retry3 difok3 minlen8 lcredit-1 dcredit-1 ucredit-1 ocredit-1 【Ubuntu系统->vim /etc/pam.d/c…

Linux组的介绍,所有者,所在组,修改所在组

目录 linux组的介绍 文件/目录所有者 组的创建 文件/目录所在组 其它组 改变用户所在组 linux组的介绍 每个用户必须属于一个组&#xff0c;不能独立于组外。 这个文件是谁创建的&#xff0c;这个文件的所有者就是谁。 这个文件属于组1&#xff0c;那么组2对它来说就…

VoLTE基本信令流程(整理的学习笔记)

VoLTE基本信令流程 1. 注册基本过程 UE进行Attach&#xff0c;建立QCI9的默认承载使用IMS APN建立PDN连接&#xff0c;建立 QCI5 的默认承载&#xff0c;用于传送 SIP 信令&#xff0c;同时获取到了IMS入口的P-CSCF地址信息UE 通过 QCI5 的默认承载向 IMS 发起注册请求IMS 域…

亚马逊ERP全功能 无隐藏收费的几个地方

亚马逊全功能 ERP 采集图片订单翻译&#xff0c;无任何隐藏收费。 说说有关采集和管理的软件&#xff01;1. 对于做跟卖的人来说需要大量采集产品&#xff0c;采集过来的产品还需要进行文本、图片翻译、修图等操作&#xff0c;图片储存和站点维护另算。市面上有些 ERP 软件在这…

美国高防服务器到底怎么选

美国高防服务器因其强大的硬件设施、高度的网络连接性、丰富的带宽资源和先进的防御技术而受到全球用户的欢迎。以下是选择美国高防服务器时需要考虑的关键因素&#xff0c;rak部落为您整理发布美国高防服务器到底怎么选。 确定服务器需求 容量和带宽&#xff1a;根据业务规模…

共享电动单车管理系统 ---附源码131016

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于共享电动单车管理系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了共享电动单车管理系统&#xff0c;它彻底…

React 学习——Class类组件生命周期函数

componentDidMount&#xff1a;组件渲染完执行一次&#xff0c;发送网络请求 componentWillUnmount&#xff1a;组件卸载的时候执行&#xff0c;副作用&#xff08;清理的工作 清除定时器 事件绑定&#xff09; import { Component,useState } from react; class Counter e…

敏捷架构开发方法和实践:迎接数字化时代的挑战

在当前飞速发展的数字化时代&#xff0c;传统企业架构的局限性日益显现&#xff0c;特别是在应用敏捷方法的过程中&#xff0c;许多企业发现其架构和组织结构无法支持真正的敏捷转型。为应对这一挑战&#xff0c;《数字化时代的敏捷架构》提出了一个全新的架构框架——敏捷架构…

Typora 软件介绍和利用

Typora 软件介绍 Typora 是一款功能强大的 Markdown 编辑器&#xff0c;以简洁直观的设计和无缝的实时预览而著称。本文将详细介绍 Typora 的功能、优势以及使用体验。 目录 简介主要功能软件优势使用体验系统支持安装与设置 简介 Typora 是一款由 Abner Lee 开发的 Markdo…

最新完整版手机APP软件应用下载类网站模板源码/手游导航源码app软件下载

源码简介&#xff1a; 手机APP软件应用下载类网站模板源码&#xff0c;它是最新完整版&#xff0c;也是手游导航源码&#xff0c;app软件排行榜下载网页源码。 这是一款带有强大后台的app软件手游类源码&#xff0c;界面设计得很漂亮&#xff0c;非常适合做app软件、手机软件…