理解多线程看这一篇就够了

一、基本概念与关系

程序
程序是含有指令和数据的文件,静态地存储在磁盘等存储设备上。它是软件的实体,但未被激活。

进程
进程是程序的一次执行过程,是系统运行程序的基本单位。当程序被操作系统加载并执行时,就成为一个进程,具有动态性。进程拥有独立的内存空间和系统资源(如CPU时间、内存、I/O设备等),是一个正在执行中的程序实例。进程之间相互独立,不共享内存空间。

线程
线程是进程内部的一个执行单元,是进程中实际运作的小单位,也是CPU调度的基本单位。相比进程,线程更轻量,同类线程共享同一块内存空间和系统资源。线程之间可以共享数据,但也可能相互影响,特别是在同一个进程中。线程的引入提高了程序的并发执行能力。

三者之间的关系

  • 程序进程:程序通过加载执行转变为进程,实现了从静态代码到动态执行实体的转变。
  • 进程线程:一个进程可以包含多个线程,这些线程共享进程的资源,在同一进程环境下并发执行不同的任务,提高了效率和响应速度。
  • 线程相比进程,降低了资源开销,提高了通信效率,但同时也可能因为资源共享引发同步问题。

二、线程的基本状态

Java 线程在其生命周期中会经历以下6种基本状态之一,并且随着代码执行和系统调度在这些状态之间转换:

  1. 新建(New):线程刚被创建,尚未启动。
  2. 可运行(Runnable):线程可以被调度执行,包括已经获得CPU时间片正在运行的线程和等待CPU分配时间片的线程。
  3. 阻塞(Blocked):线程等待某个监视器锁(例如进入synchronized代码块时),或者等待I/O操作完成等。
  4. 等待(Waiting):线程等待其他线程执行特定操作,如调用Object.wait()方法,不可被中断直到等待条件满足。
  5. 超时等待(Timed Waiting):与等待状态相似,但线程在指定时间后会自动醒来,如调用Thread.sleep(long millis)方法。
  6. 终止(Terminated):线程已结束执行,无论是正常结束还是异常结束。

这些状态描述了线程从创建到执行完毕的完整生命周期,理解这些状态对于调试多线程程序和避免死锁等问题至关重要。
Java 线程状态如下图所示:
在这里插入图片描述

三、 线程池的原理与创建原因及方式

原理

线程池是一种基于池化思想管理线程的机制,其核心在于重用线程资源,减少创建和销毁线程的开销。线程池的工作流程主要包括以下几个步骤:

  1. 创建线程池:初始化时,预先创建一定数量的线程并将其置于待命状态,等待任务分配。
  2. 任务提交:当有新任务到达时,将其加入到任务队列中。
  3. 任务调度:线程池中的空闲线程会从任务队列中取出任务并执行。若所有线程均在忙状态,且任务队列已满,则根据策略决定是否创建新线程或拒绝任务。
  4. 任务执行完毕:线程不会立即销毁,而是返回线程池等待下一个任务。
  5. 线程管理:根据系统负载动态调整线程数量,避免过多线程导致资源耗尽或过少线程影响任务处理速度。
优点
  1. 资源重用:通过重复利用已创建的线程,减少了线程创建和销毁的开销。
  2. 提高响应速度:任务到达时无需等待新线程创建即可立即执行。
  3. 管理灵活性:可根据系统状况动态调整线程数量,有效控制资源使用,防止资源耗尽。
  4. 简化编程模型:提供统一的接口给开发者提交任务,隐藏了线程管理的复杂细节。
  5. 提高系统稳定性:通过控制最大线程数,防止了过多线程导致的资源竞争和调度开销,增强了系统的稳定性和可预测性。
创建线程池的方式
  1. 手动创建:基础方式,使用语言提供的线程创建API(如Java中的Thread类)配合同步机制(如队列、锁等)自行实现线程池逻辑。

  2. 使用标准库

    • Java: 通过Executors类提供的工厂方法创建不同类型线程池,如newFixedThreadPool创建固定大小线程池,newCachedThreadPool创建可缓存线程池等。
    • C++/POSIX: 利用std::thread结合std::queue等容器自建线程池,或使用第三方库如boost::thread_pool
    • 其他语言:如Python的concurrent.futures.ThreadPoolExecutor,Node.js的worker_threads模块等,都提供了线程池或类似机制的封装。
  3. 第三方库:使用成熟的第三方线程池库,如Java的Apache Commons ThreadPoolExecutor,C#的Microsoft TPL(Task Parallel Library)等,这些库通常提供了更高级的特性,如线程池监控、任务调度策略等。

选择合适的创建方式需根据项目需求、性能要求以及目标平台的支持程度综合考虑。

四、线程池的创建与参数解析

在Java中,通过ThreadPoolExecutor类来创建自定义线程池,其构造函数提供了高度灵活的配置选项,以便根据具体需求调整线程池的行为。下面是构造函数及其参数的详细介绍:

 
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 空闲线程存活时间
    TimeUnit unit,         // 存活时间的单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    RejectedExecutionHandler handler  // 拒绝策略
)
参数解释
  1. corePoolSize核心线程数
    线程池中常驻的核心线程数量。即使线程空闲,这部分线程也会保留在线程池中,不会被回收。只有在工作队列满并且当前线程数小于最大线程数时才会创建新的线程。

  2. maximumPoolSize最大线程数
    线程池能容纳的最大线程数量。当活动线程达到这个数值后,新来的任务如果不能放入任务队列将被拒绝处理。

  3. keepAliveTime空闲线程存活时间
    当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务最长时间达到keepAliveTime后,如果还没有新任务到来,那么这些线程将被终止以节省资源。

  4. unit存活时间的单位
    keepAliveTime参数的时间单位,如TimeUnit.SECONDS秒、TimeUnit.MILLISECONDS毫秒等。

  5. workQueue任务队列
    用于保存等待执行的任务的阻塞队列。不同的队列实现会影响线程池的行为,常见的有ArrayBlockingQueue(固定大小)、LinkedBlockingQueue(无界或有限界限)、SynchronousQueue(直接传递,没有容量)等。

  6. handler拒绝策略
    当线程池和任务队列都达到饱和状态时(即无法再接受新任务),如何处理新提交的任务。JDK内置了几种拒绝策略:

    • AbortPolicy:默认策略,丢弃任务并抛出RejectedExecutionException异常。
    • CallerRunsPolicy:调用者运行策略,直接在调用者线程中执行被拒绝的任务。
    • DiscardPolicy:静默丢弃任务,不抛出任何异常。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,并尝试重新提交当前任务。

通过调整这些参数,可以创建符合特定应用场景需求的线程池,以达到资源最优利用和任务高效执行的目的。

五、 线程池原理

1. 固定大小线程池(FixedThreadPool)

特点

固定大小线程池维护一个固定数量的线程,确保所有任务都会在一个控制好的线程集合中执行,适合于负载较为稳定、任务执行时间相对均衡的场景。由于使用了无界队列LinkedBlockingQueue,如果任务提交速率超过处理能力,队列可能会无限增长,导致内存溢出风险。

参数分析:

  • corePoolSize:等于最大线程数,一旦创建就不会改变,保证线程数量恒定。

  • workQueue:默认为LinkedBlockingQueue,队列容量无上限。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
    }
}
2. 可缓存线程池(CachedThreadPool)

特点:

可缓存线程池会根据任务的需求动态创建线程,空闲线程超过一定时间(默认60秒)则会被回收。这种线程池适合执行大量短期异步任务,能够迅速响应突发请求,但不适合长时间运行的任务,因为线程数量可能无限增长,导致资源耗尽。

参数分析:

  • corePoolSize=0,初始时无核心线程。
  • maximumPoolSize=Integer.MAX_VALUE,理论上允许无限增长。
  • keepAliveTime=1分钟,空闲线程等待新任务的最长时间。
  • workQueue=SynchronousQueue,直接传递,不保留任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
            });
        }
        executor.shutdown();
    }
}
3. 定时任务线程池(ScheduledThreadPool)

特点:

定时任务线程池用于执行定时或周期性的任务,如定时检查、定时清理等。它维护了一个固定大小的核心线程池,并使用DelayedWorkQueue作为任务队列来存放将要执行的任务。

参数分析:

  • corePoolSize:线程池的基本大小,即使没有任务执行,线程也会保持存活。
  • 支持schedule系列方法设定任务的执行时间或周期。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolDemo {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 3; i++) {
            final int taskId = i;
            executor.scheduleAtFixedRate(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
            }, 0, 1, TimeUnit.SECONDS);
        }
    }
}

以上介绍了Java中三种常用的线程池类型,通过实例展示了它们的使用方法和特性。在实际应用中,应根据具体需求选择合适的线程池类型,并考虑是否需要进一步自定义线程池参数,以达到最佳的性能与资源利用率。尽管Executors类提供了简便的工厂方法,但在生产环境中推荐直接使用ThreadPoolExecutor构造函数来实现更细致的控制,以避免潜在的资源耗尽问题

六、常用的线程池

提交一个任务到线程池中,线程池的处理流程如下:

  • 1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创
    建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  • 2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如
    果工作队列满了,则进入下个流程。
  • 3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
1、ThreadPoolExecutor的execute()方法
public void execute(Runnable command) {
    if (command == null) {
        throw new NullPointerException();
    }
    // 如果线程数大于等于核心线程数或者线程创建失败,将任务加入队列
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        // 线程池处于运行状态并且加入队列成功
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0) {
                ensureQueuedTaskHandled(command);
            }
        } 
        // 线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
        else if (!addIfUnderMaximumPoolSize(command)) {
            // 创建线程失败,则采取阻塞处理的方式
            reject(command); // is shutdown or saturated
        }
    }
}
2、创建线程的方法:addIfUnderCorePoolSize(command)
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING) {
            t = addThread(firstTask);
        }
    } finally {
        mainLock.unlock();
    }
    if (t == null) {
        return false;
    }
    t.start();
    return true;
}
我们重点来看第7行:
private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);
    if (t != null) {
        w.thread = t;
        workers.add(w);
        int nt = ++poolSize;
        if (nt > largestPoolSize) {
            largestPoolSize = nt;
        }
    }
    return t;
}

这里将线程封装成工作线程worker,并放入工作线程组里,worker类的方法run方法:

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

worker在执行完任务后,还会通过getTask方法循环获取工作队里里的任务来执行。 我们通过一个程序来观察线程池的工作原理:

3、创建一个线程
public class ThreadPoolTest implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4、线程池循环运行11个线程:
public static void main(String[] args) {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            5, // 核心线程数
            10, // 最大线程数
            60, // 空闲线程存活时间
            TimeUnit.SECONDS, // 时间单位
            queue // 任务队列
    );

    for (int i = 0; i < 11; i++) {
        threadPool.execute(
                new Thread(new ThreadPoolTest(), "Thread" + i)
        );
        System.out.println("活跃的线程数: " + threadPool.getPoolSize());
        if (queue.size() > 0) {
            System.out.println("----------------队列阻塞的线程数" + queue.size());
        }
    }

    threadPool.shutdown();
}

执行结果:

活跃的线程数: 1
活跃的线程数: 2
活跃的线程数: 3
活跃的线程数: 4
活跃的线程数: 5
活跃的线程数: 5
----------------队列阻塞的线程数1
活跃的线程数: 5
----------------队列阻塞的线程数2
活跃的线程数: 5
----------------队列阻塞的线程数3
活跃的线程数: 5
----------------队列阻塞的线程数4
活跃的线程数: 5
----------------队列阻塞的线程数5
活跃的线程数: 6
----------------队列阻塞的线程数5
活跃的线程数: 7
----------------队列阻塞的线程数5
活跃的线程数: 8
----------------队列阻塞的线程数5
活跃的线程数: 9
----------------队列阻塞的线程数5
活跃的线程数: 10
----------------队列阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task
Thread[Thread15,5,main] rejected from
java.util.concurrent.ThreadPoolExecutor@232204a1[Running, pool size = 10, active
threads = 10, queued tasks = 5, completed tasks = 0]
at
java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPool
Executor.java:2047)
at
java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at test.ThreadTest.main(ThreadTest.java:17)
分析结果

观察与分析

  1. 线程池配置概览:创建的线程池具体配置为:核心线程数量为5个;最大线程总数为10个;关联的工作队列容量为5个任务。

  2. 工作队列监控:通过queue.size()方法实时监测工作队列中的任务数量,帮助理解线程池的工作状态。

  3. 运行机制解析:

  • 初始阶段,随着任务的提交,线程池会创建核心线程直至达到5个。
  • 当核心线程满载后,新任务被加入到工作队列中,直至队列也达到其容量上限5个。
  • 队列饱和后,线程池会继续创建非核心线程,直至线程总数达到最大限制10个。
  • 若线程数和队列均达到上限,此时线程池进入饱和状态,需依据饱和策略处理后续提交的任务。

饱和策略(RejectedExecutionHandler)调整
当线程池和队列均达到饱和,即无法接纳新任务时,JDK提供了四种预设的饱和策略处理新提交的任务:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:将任务回退给调用线程直接执行。
  3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试重新提交当前任务。
  4. DiscardPolicy:直接丢弃新提交的任务,不抛出异常。
    修改示例:现在,我们将上述示例中的饱和策略改为DiscardPolicy,即丢弃新提交的任务而不抛出异常。
public static void main(String[] args) {
    // ... 线程池初始化代码保持不变 ...
    
    // 设置饱和策略为DiscardPolicy
    threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

    // 任务提交逻辑保持不变 ...
    
    threadPool.shutdown();
}

基于之前的讨论,让我们整合并扩展这部分内容,包括修改线程池的饱和策略为DiscardPolicy并进行相应排版:

观察与分析

  1. 线程池配置概览:创建的线程池具体配置为:核心线程数量为5个;最大线程总数为10个;关联的工作队列容量为5个任务。

  2. 工作队列监控:通过queue.size()方法实时监测工作队列中的任务数量,帮助理解线程池的工作状态。

  3. 运行机制解析

    • 初始阶段,随着任务的提交,线程池会创建核心线程直至达到5个。
    • 当核心线程满载后,新任务被加入到工作队列中,直至队列也达到其容量上限5个。
    • 队列饱和后,线程池会继续创建非核心线程,直至线程总数达到最大限制10个。
    • 若线程数和队列均达到上限,此时线程池进入饱和状态,需依据饱和策略处理后续提交的任务。

饱和策略(RejectedExecutionHandler)调整

当线程池和队列均达到饱和,即无法接纳新任务时,JDK提供了四种预设的饱和策略处理新提交的任务:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:将任务回退给调用线程直接执行。
  3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试重新提交当前任务。
  4. DiscardPolicy:直接丢弃新提交的任务,不抛出异常。

修改示例:现在,我们将上述示例中的饱和策略改为DiscardPolicy,即丢弃新提交的任务而不抛出异常。

public static void main(String[] args) {
    // ... 线程池初始化代码保持不变 ...
    
    // 设置饱和策略为DiscardPolicy
    threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

    // 任务提交逻辑保持不变 ...
    
    threadPool.shutdown();
}

通过这样的调整,当线程池和队列达到饱和状态后,任何新提交的任务将被默默地丢弃,这种方式适用于那些可以安全丢弃的任务场景,避免了因任务拒绝而引发的异常中断,提高了程序的健壮性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SAP 消息号VF501科目确定期间出错

在销售开票VF02的时候&#xff0c;经常出现报错&#xff1a;“科目确定期间出错”&#xff0c;这个报错&#xff0c;目前检查步骤&#xff1a; 1、BP客户主数据&#xff0c;销售代码层数据&#xff08;销售与分销&#xff09;-开票-会计-客户科目分配组&#xff0c;要与销售订…

卷积计算过程详解(含图示和代码)

什么是卷积&#xff1f; 卷积是一种数学运算&#xff0c;通过两个函数f和g生成第三个函数&#xff0c;其本质是一种特殊的积分变换&#xff0c;表征函数f与g经过翻转和平移的重叠部分函数值乘积对重叠长度的积分。卷积在泛函分析中扮演重要角色&#xff0c;可以被看作是“滑动平…

【大数据】Hadoop 2.X和1.X升级优化对比

目录 1.前言 2.hadoop 1.X的缺点和优化方向 3.解决NameNode的局限性 3.1.Hadoop HA 3.2.Haddop federation 4.yarn 5.周边组件 1.前言 本文是作者大数据系列中的一文&#xff0c;专栏地址&#xff1a; https://blog.csdn.net/joker_zjn/category_12631789.html?spm10…

扎气球最高分-第13届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第74讲。 扎气球最高分&…

图片怎样在线改像素大小?电脑快速修改图片大小的方法

在设计图片的时候下载的图片尺寸一般会比较大&#xff0c;在网上使用经常会因为尺寸的问题导致无法正常上传&#xff0c;那么如何快速在线改图片大小呢&#xff1f;想要修改图片尺寸可以在直接选择网上的图片改大小工具的功能来快速完成修改&#xff0c;操作简单方便使用&#…

FreeRTOS学习笔记-基于stm32(7)任务状态查询与任务时间统计API函数

1、FreeRTOS任务相关API函数 函数描述uxTaskPriorityGet()查询某个任务的优先级vTaskPrioritySet()改变某个任务的任务优先级uxTaskGetSystemState()获取系统中任务状态vTaskGetInfo()获取某个任务信息xTaskGetApplicationTaskTag()获取某个任务的标签(Tag)值xTaskGetCurrentT…

0.25W 1.5KVDC~3KVDC 隔离超小型单输出 DC/DC 电源模块——TKE-W25系列

TKE-W25系列隔离超小型单输出 DC/DC 电源模块是一款超小型单输出电源模块&#xff0c;工业级环境温度&#xff0c;用于PCB安装的国际标准结构。此系列产品小巧&#xff0c;效率高&#xff0c;低输出纹波,用于需要电压转换和隔离的场合&#xff0c;封装有SIP和DIP可选。

MiniPCIe/SATA双用插槽无法识别minipcie模块怎么回事!

在计算机和嵌入式系统设计中,MiniPCIe/SATA双用插槽作为一种高度集成的解决方案,提供了极大的灵活性与扩展能力。它不仅能够支持MiniPCIe接口的无线网卡、固态硬盘控制器等模块,还能适应SATA接口的硬盘或固态存储设备,大大丰富了系统配置的可能性。尽管设计初衷良好,但在实…

Java—二分查找

介绍 二分查找&#xff08;Binary Search&#xff09;是一种在有序数组中查找特定元素的搜索算法。其基本思想是将目标值与数组中间的元素进行比较&#xff1a; 如果目标值等于中间元素&#xff0c;则查找成功。如果目标值小于中间元素&#xff0c;则在数组左半部分继续进行二…

汽车悬架分为哪几类

汽车悬架分为哪几类 1)汽车的悬架系统可根据结构分为两种:独立悬架和非独立悬架,独立悬架根据构造又可以分为CDC运动悬架(CDC电磁悬架系统)和空气悬架; 2)当前比较火热的空气悬架,是独立悬架的一种; 3)前轮主要使用麦弗逊式独立悬架 和 双叉臂悬架,后轮主要使用多…

基于 DCT 的图像滤波

需求分析 对于图像去噪这一需求&#xff0c;我们可以通过DCT&#xff08;离散余弦变换&#xff09;算法来实现。DCT是一种基于频域的变换技术&#xff0c;可以将图像从空间域转换为频域&#xff0c;然后通过滤波等处理方式进行去噪。 针对这一需求&#xff0c;我们需要进行以下…

香港优才计划申请时间要多久?各流程申请周期规划,再晚就来不及了!

香港优才计划申请时间要多久&#xff1f;各流程申请周期规划&#xff0c;再晚就来不及了&#xff01; 2024年是香港优才计划不限配额的最后一年&#xff0c;明年政策如何变化还未可知&#xff0c;但如果明年又设置限额了&#xff0c;那么今年最后的机会一定要抓住了。 在这里…

美业SaaS收银系统源码-美团/口碑核销时报错:该商品未在美团/口碑上架怎么办?

美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 活动促销 PC管理后台、手机APP、iPad APP、微信小程序 1. 可能是门店未做映射 • 美团门店映射&#xff1a;需要在【PC运营后端】-【渠道商品】-【美团点评门店管理】&…

elementUI type=“selection“多选框选中 删除 回显 赋值问题 回显数组改变选中状态未改变

业务需求&#xff1a; 点击查询弹列表框 勾选列表选项保存 可删除可重新查询列表添加 遇到的问题&#xff1a;删除之后查询列表selection回显问题 解决&#xff1a;row-click配合:reserve-selection"true"使用 <el-tableref"refPlanTable":data"…

AI时代的服装设计师--AIGC

AI时代的服装设计师--AIGC AIGCAIGC设计能替代真正的设计师吗森马T恤设计AIGC优势、优化 本文记录于去年参加的一次森马T恤设计活动的感受。 AIGC 可以说&#xff0c;近期以来&#xff0c;随着ChatGPT的不断发展&#xff0c;从ChatGPT-3到ChatGPT-4的飞速发展&#xff0c;AIGC…

【Spring Cloud】分布式配置

目录 未来的开发场景为什么需要配置中心配置实时生效配置管理流程 开源配置中心基本介绍DisconfSpring Cloud ConfigApolloNacos Spring Cloud Config介绍配置管理工具体系 案例需求编写 Config Server1.创建配置文件2.创建项目3.添加依赖4.添加注解5.修改配置文件application.…

selenium web 网页测试自动化需要哪些技术?

引言&#xff1a; 在当今互联网时代&#xff0c;网页测试自动化成为了确保软件质量和提高效率的重要手段之一。Selenium是一种功能强大且广泛应用的工具&#xff0c;可用于实现网页测试自动化。本文将带您了解Selenium Web网页测试自动化所需的技术和步骤&#xff0c;以便您从零…

伦敦银和现货白银是一回事吗

伦敦银和现货白银不能直接完全地画上等号&#xff0c;但如果投资者所指指的是国际市场上的现货白银交易&#xff0c;那么二者应该是等同的——因为在国际贵金属投资市场上&#xff0c;现货白银的别称就是伦敦银&#xff0c;伦敦银和现货白银指的其实是同一回事。 因为早在很多个…

MySQL学习——连接服务器和输入查询

MySQL是一个流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;由瑞典的MySQL AB公司开发&#xff0c;后来被Oracle公司收购。它使用SQL&#xff08;结构化查询语言&#xff09;作为访问和操作数据库的标准语言。 要查看 mysql 客户端程序提供的选项列表&a…