ThreadPoolExecutor线程池详解

ThreadPoolExecutor线程池详解

1. 背景

项目最近的迭代中使用到了ThreadPoolExecutor线程池,之前都只是知道怎么用,没有了解过线程池的底层原理,项目刚上线,有时间整理一下线程池的用法,学习一下线程池的底层实现与工作原理。

2. ThreadPoolExecutor工作原理

2.1 构造方法

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

2.2 线程池的使用

  • worker
/**
 * @author itender
 * @date 2023/8/7 14:41
 * @desc
 */
public class Worker implements Runnable {

    private String command;

    public Worker(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + command + " startTie = " + DateUtil.now());
        processCommand();
        System.out.println(Thread.currentThread().getName() + command +  " endTime = " + DateUtil.now());
    }

    private void processCommand() {
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + command +" 处理任务逻辑。。。。。。。。");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}
  • 线程池
/**
 * @author itender
 * @date 2023/8/7 14:37
 * @desc
 */
public class ThreadPoolExecutorDemo {
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程数 5
                CORE_POOL_SIZE,
                // 最大线程数 10
                MAX_POOL_SIZE,
                // 超过核心线程数,线程最大存活时间
                KEEP_ALIVE_TIME,
                // 时间单位
                TimeUnit.MINUTES,
                // 工作队列最大值
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
            	// 线程工厂,创建线程的时候使用
                r -> {
                    Thread thread = new Thread(r);
                    thread.setName("pool-");
                    return thread;
                },
        new ThreadPoolExecutor.CallerRunsPolicy()
        );
        for (int i = 0; i < 10; i++) {
            // 创建任务
            Worker myRunnable = new Worker("" + i);
            // 执行任务
            threadPoolExecutor.execute(myRunnable);
        }
        // 种植线程池,不接受新任务,但是有工作线程处理队列中的任务
        threadPoolExecutor.shutdown();

        while (!threadPoolExecutor.isTerminated()) {

        }
        System.out.println("Finished All Threads!");
    }
}

2.3 核心参数

2.3.1 核心参数详解

  • corePoolSize:核心线程数,任务队列没有达到队列最大容量时,最大可以同时运行的线程数。
  • maximumPoolSize:最大线程数。当任务队列中存储的任务达到队列的容量时,当前可以同时运行的线程数量变为最大线程数。
  • keepAliveTime:线程池中的线程数量超过corePoolSize时,如果没有新任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待的时间超过了keepAliveTime才会被销毁回收。
  • unitkeepAliveTime参数的时间单位。
  • workQueue:工作队列。当有新的任务提交的时候,会先判断当前运行的线程数是否达到核心线程数,如果达到核心线程数,则会把新提交的任务放到工作队列中。
  • threadFactory:线程工厂,创建新的线程时会使用。
  • handler:拒绝策略。

2.3.2 拒绝策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • AbortPolicy:默认拒绝策略。抛出RejectExecutionException来拒绝新任务的处理。
  • CallerRunsPolicy:调用当前提交任务的线程来执行任务。一般不希望任务丢失会选用这种策略,但从实际角度来看,原来的异步调用意图会退化为同步调用。
  • DiscardPolicy:不处理新任务,直接丢弃。
  • DiscardOldestPolicy:丢弃最早的未处理的任务。

2.4 执行流程

在这里插入图片描述

2.5 线程池状态

2.5.1 线程池核心属性ctl

	// ctl本质是 Integer 型变量,进行了原子性的封装
	// ctl表示两种状态:
	// 高3位:线程池当前的状态
	// 低29位:线程池当前工作线程的数量
	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
	// COUNT_BITS 的值为 29(整型Integer.SIZE = 32 位);
    private static final int COUNT_BITS = Integer.SIZE - 3;
	// CAPACITY = (1 << 29) - 1; 1左移29位,减去1;即1*2^29-1;
	// 0001 1111 1111 1111 1111 1111 1111 1111
	// 低29位用来表示线程池的最大线程容量
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
	
	// 高3位用来表示线程池5种状态
	// 111 运行状态
    private static final int RUNNING    = -1 << COUNT_BITS;
	// 000 shutdown状态
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
	// 001 停止状态
    private static final int STOP       =  1 << COUNT_BITS;
	// 010 过渡状态
    private static final int TIDYING    =  2 << COUNT_BITS;
	// 011 中介状态
    private static final int TERMINATED =  3 << COUNT_BITS;
	// 根据ctl的值,计算当前线程池的状态
	// 计算方式:c 与 非capacity
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
	// 根据ctl的值,计算线程池当前运行的线程的容量
    private static int workerCountOf(int c)  { return c & CAPACITY; }
	// 通过运行状态和工作线程数计算ctl的值,或运算
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }

    /**
     * Attempts to CAS-increment the workerCount field of ctl.
     */
    private boolean compareAndIncrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect + 1);
    }

    /**
     * Attempts to CAS-decrement the workerCount field of ctl.
     */
    private boolean compareAndDecrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect - 1);
    }

    /**
     * Decrements the workerCount field of ctl. This is called only on
     * abrupt termination of a thread (see processWorkerExit). Other
     * decrements are performed within getTask.
     */
    private void decrementWorkerCount() {
        do {} while (! compareAndDecrementWorkerCount(ctl.get()));
    }

2.5.2 状态切换

在这里插入图片描述

  • RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown() 方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用 shutdown() 方法进入该状态)。
  • STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
  • TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入 TERMINATED 状态。
  • TERMINATED:在terminated()方法执行完后进入该状态,默认 terminated() 方法中什么也没有做。

3. 源码分析

3.1 execute方法

  • 源码
public void execute(Runnable command) {
    	// 判断任务是否为空,如果任务为空,抛出空指针异常
        if (command == null)
            throw new NullPointerException();
    	// 获取ctl属性
        int c = ctl.get();
    	// 判断当前工作线程数量是否小于核心线程的数量
        if (workerCountOf(c) < corePoolSize) {
            // 工作线程数小于核心线程数,创建一个核心线程执行command任务
            if (addWorker(command, true))
                // 创建核心线程成功,直接返回
                return;
            // 并发情况下添加核心线程失败,需要重新获取ctl属性
            c = ctl.get();
        }
    	// 创建核心线程失败,当前工作线程数量大于或等于核心线程数量corePoolSize
    	// 判断线程池的状态是否为running,如果是添加任务到工作队列中(放入任务失败返回false)
        if (isRunning(c) && workQueue.offer(command)) {
            // 任务添加到队列成功,再次获取ctl属性
            int recheck = ctl.get();
            // 二次检查,判断线程池的状态是否为running,如果不是队列中移除刚刚添加的任务
            if (!isRunning(recheck) && remove(command))
                // 执行拒绝策略
                reject(command);
            // 1.任务添加到队列
            // 2.线程池可能是running状态
            // 3.传入的任务可能从任务队列中移除失败(移除失败的唯一可能就是任务已经被执行了)
            // 判断工作线程数量是否为0
            else if (workerCountOf(recheck) == 0)
                // 工作线程数量为0
                // 工作队列中有任务在排队,添加一个空任务,创建非核心线程执行队列中等待的任务
                addWorker(null, false);
        }
    	// 创建核心线程失败,
    	// 线程池状态不是running状态
    	// 线程池可能是running状态,但是任务队列已经满了
    	// 添加任务到工作队列失败,创建非核心线程执行任务
        else if (!addWorker(command, false))
            // 创建非核心线程失败,执行拒绝策略
            reject(command);
    }

第一点核心:通过execute方法源码可以看出线程池具体的执行流程,以及一些避免并发情况的判断。

第二点核心:线程池为什么会添加空任务非核心线程到线程池。

这里是一个疑惑点:为什么需要二次检查线程池的运行状态,当前工作线程数量为0,尝试创建一个非核心线程并且传入的任务对象为null?这个可以看API注释:

如果一个任务成功加入任务队列,我们依然需要二次检查是否需要添加一个工作线程(因为所有存活的工作线程有可能在最后一次检查之后已经终结)或者执行当前方法的时候线程池是否已经shutdown了。所以我们需要二次检查线程池的状态,必须时把任务从任务队列中移除或者在没有可用的工作线程的前提下新建一个工作线程。

3.2 addWorker方法

  • 源码
private boolean addWorker(Runnable firstTask, boolean core) {
    // for循环标识
    // 对线程池当前状态和当前工作线程数量的判断
    retry:
    for (;;) {
        // 获取线程池的状态
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            // 获取线程池工作线程的数量
            int wc = workerCountOf(c);
             // 1. 如果传入的core为true,表示将要创建核心线程,通过wc和corePoolSize判断,如果wc >= corePoolSize,则返回false表示创建核心线程失败
            // 2. 如果传入的core为false,表示将要创非建核心线程,通过wc和maximumPoolSize判断,如果wc >= maximumPoolSize,则返回false表示创建非核心线程失败
            // core参数为false说明工作队列已经满了,线程池大小变为maximumPoolSize最大线程数
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // CAS更新工作线程数wc,原子操作将workCount的数量加1,更新成功则直接跳出最外层循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // CAS更新工作线程数失败,判断线程池的状态是否从running编程shutdown,如果线程池的状态改变了在执行上面的操作
            c = ctl.get();  // Re-read ctl
            // 如果线程池状态已经变成shutdown,跳过最外层本次循环,执行下一次循环
            if (runStateOf(c) != rs)
                continue retry;
            // 如果线程池状态依然是RUNNING,CAS更新工作线程数wc失败说明有可能是并发更新导致的失败,则在内层循环重试即可 
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
	// 工作线程是否启动成功
    boolean workerStarted = false;
    // 工作线程是否创建成功
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 加锁,因为会改变一些指标值和非线程安全的集合
            final ReentrantLock mainLock = this.mainLock;
            // 加锁
            mainLock.lock();
            try {
                // 获取线程池状态
                int rs = runStateOf(ctl.get());
				//rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中
        		//(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
                // 对于2,换言之,如果线程池处于SHUTDOWN状态下,同时传入的任务实例firstTask不为null,则不会添加到工作线程集合和启动新的Worker
                // 这一步其实有可能创建了新的Worker实例但是并不启动(临时对象,没有任何强引用),这种Worker有可能成功下一轮GC被收集的垃圾对象
        		// firstTask == null证明只新建线程而不执行任务
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // 将新建的工作线程添加到工作线程的集合
                    workers.add(w);
                    // 更新当前工作线程的最大容量
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 工作线程是否添加成功
                    workerAdded = true;
                }
            } finally {
                // 释放锁
                mainLock.unlock();
            }
            // 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
            if (workerAdded) {
                // 启动线程,标识线程启动成功
                t.start();
                workerStarted = true;
            }
        }
    } finally {
         // 线程启动失败,需要从工作线程中移除对应的Worker
        if (!workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

4. 线程池常见问题

4.1 execute()submit()的区别

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException

4.3 阻塞队列的作用

  • 一般的队列只能是有限长度的缓冲区,一旦超出缓冲长度,就无法保留了。阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

  • 阻塞队列可以在队列中没有任务时,阻塞想要获取任务的线程,使其进入wait状态,释放cpu资源。

  • 阻塞队列带有阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。

4.2 为什么先添加队列而不是先创建最大线程

  • 在创建新线程的时候,是要获取全局锁的,这时候其他线程会被阻塞,影响整体效率。

  • 在核心线程已满时,如果任务继续增加那么放在队列中,等队列满了而任务还在增加那么就要创建临时线程了,这样代价低。

5. 参考文章

https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/

https://javaguide.cn/java/concurrent/java-thread-pool-summary.html#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90

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

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

相关文章

局域网共享文件夹怎么加密?共享文件夹加密软件盘点

局域网共享文件夹可以提高企业的沟通效率&#xff0c;使数据交流更加方便&#xff0c;但同时也增大了数据泄露的风险。那么局域网共享文件夹怎么加密呢&#xff1f;下面我们就来了解一下。 局域网共享文件夹加密设置方法 普通的文件夹加密软件仅适用于电脑本地文件夹&#xff…

01_什么是ansible、基本架构、ansible工作机制、Ansible安装、配置主机清单、设置SSH无密码登录等

1.什么是ansible 1.1.基本介绍 1.2.基本架构 1.3.基本特征 1.4.优点 1.5.ansible工作机制 2.Ansible安装 2.1.机器准备 2.2.安装ansible 2.2.1.安装epel源 2.2.2.安装ansible 2.2.3.查看ansible版本 2.2.4.树状结构展示文件夹 2.2.4.1.其中ansible.cfg的内容如下 2.2.4.2.host的…

24届近5年上海大学自动化考研院校分析

今天给大家带来的是上海大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、上海大学 学校简介 上海大学是上海市属的综合性研究型大学&#xff0c;是教育部与上海市人民政府共建高校&#xff0c;是国家“211 工程”重点建设高校、上海市高水平地方大学建设高校&a…

【Kubernetes部署篇】基于Ubuntu20.04操作系统搭建K8S1.23版本集群

文章目录 一、集群架构规划信息二、系统初始化准备(所有节点同步操作)三、安装kubeadm(所有节点同步操作)四、初始化K8S集群(master节点操作)五、添加Node节点到K8S集群中六、安装Calico网络插件七、测试CoreDNS可用性 一、集群架构规划信息 pod网段&#xff1a;10.244.0.0/16…

中断子系统--硬件层(GICv3)

目录 综述 硬件层--GICV3 中断类型 中断状态 Distributor组件 中断使能配置 中断触发方式配置 中断优先级配置  中断分组标记 GIC处理中断流程 综述 由上面的block图&#xff0c;我们可知linux kernel的中断子系统分成4个部分&#xff1a; 硬件层&#xff1a;最下层…

Abaqus 中最常用的子程序有哪些 硕迪科技

在ABAQUS中&#xff0c;用户定义的子程序是一种重要的构件&#xff0c;可以将其插入到Abaqus分析中以增强该软件的功能和灵活性。这些子程序允许用户在分析过程中添加自定义材料模型、边界条件、初始化、加载等特定操作&#xff0c;以便更精准地模拟分析中的现象和现象。ABAQUS…

小研究 - MySQL 分区技术在海量系统日志中的应用

随着信息技术的飞速发展&#xff0c;系统的业务功能不断扩大&#xff0c;产生的日志与日俱增&#xff0c;导致应用软件的运行速度越来越慢&#xff0c;不能很好地满足用户对软件性能的需求。基于此&#xff0c;重点研究了 MySQL 分区技术在大数据量软件日志中的应用&#xff0c…

解决Vue+Element-UI 进行From表单校验时出现了英文提示问题

说明&#xff1a;该篇博客是博主一字一码编写的&#xff0c;实属不易&#xff0c;请尊重原创&#xff0c;谢谢大家&#xff01; 问题描述 在使用form表单时&#xff0c;往往会对表单字段进行校验&#xff0c;字段为必填项时会添加required属性&#xff0c;此时自定义rules规则…

伪原创小发猫怎么样【php源码】

大家好&#xff0c;小编为大家解答初学者自学python哪本书好的问题。很多人还不知道自学python需要什么基础&#xff0c;现在让我们一起来看看吧&#xff01; 火车头采集ai伪原创插件截图&#xff1a; 目前python可以说是一门非常火爆的编程语言&#xff0c;应用范围也非常的广…

Python web实战之Django的文件上传和处理详解

概要 关键词&#xff1a;Python Web开发、Django、文件上传、文件处理 今天分享一下Django的文件上传和处理。 1. 上传文件的基本原理 在开始深入讲解Django的文件上传和处理之前&#xff0c;先了解一下文件上传的基本原理。当用户选择要上传的文件后&#xff0c;该文件会被发…

C++动态规划经典试题解析之打家劫舍系列

1.前言 力扣上有几道与打家劫舍相关的题目,算是学习动态规划时常被提及的经典试题,很有代表性,常在因内大大小小的社区内看到众人对此类问题的讨论。 学习最好的方式便是归纳总结、借鉴消化,基于这个目的,本文对此类问题也做了讲解,在一些优秀思想的基础上添加了个人观…

网络安全进阶学习第十二课——SQL手工注入3(Access数据库)

文章目录 注入流程&#xff1a;1、判断数据库类型2、判断表名3、判断列名4、判断列数1&#xff09;判断显示位 5、判断数据长度6、爆破数据内容 注入流程&#xff1a; 判断数据库类型 ——> 判断表名 ——> 判断列名 ——> 判断列名长度 ——> 查出数据。 asp的网…

【flink】Checkpoint expired before completing.

使用flink同步数据出现错误Checkpoint expired before completing. 11:32:34,455 WARN org.apache.flink.runtime.checkpoint.CheckpointFailureManager [Checkpoint Timer] - Failed to trigger or complete checkpoint 4 for job 1b1d41031ea45d15bdb3324004c2d749. (2 con…

用excel格式书写的接口用例执行脚本

创建测试用例和测试结果集文件夹&#xff1a; excel编写的接口测试用例如下&#xff1a; 1 encoding 响应的编码格式。所测项目大部分是utf-8&#xff0c;有一个特殊项目是utf-8-sig 2 params 对应requests的params 3 data&#xff0c;对应requests的data 有些参数是动态的&a…

JVM分析工具JProfiler介绍及安装

目录 一、什么是JProfiler&#xff1f; 二、JProfiler 功能结构 1、分析代理 2、记录数据 3、快照 三、安装 一、什么是JProfiler&#xff1f; JProfiler是一个专业的工具&#xff0c;用于分析运行中的JVM内部发生的事情。当您的生产系统出现问题时&#xff0c;您可以…

Kotlin基础(十一):反射和注解

前言 本文主要讲解kotlin反射和注解。 Kotlin文章列表 Kotlin文章列表: 点击此处跳转查看 目录 1.1 kotlin反射 1.1.1 kotlin反射概念和常见使用场景 在Kotlin中&#xff0c;反射是一种能够在运行时动态地获取、检查和操作类、属性、方法等结构的能力。Kotlin为反射提供了一…

整数规划——第七章 分支定界算法

整数规划——第七章 分支定界算法 目前大部分整数规划商业软件如CPLEX&#xff0c;Gurobi和BARON等都是基于分枝定界算法框架的。 7.1 最优性条件和界 考虑下列一般线性整数规划问题&#xff1a; (IP) min ⁡ c T x , s . t . A x ≤ b , x ∈ Z n (7.1) \text{(IP)}\quad…

接口测试——postman接口测试(三)

目录 1. postman介绍与安装 2. postman发送get请求 3. postman发送post请求 1. postman介绍与安装 安装网址&#xff1a;Postman安装教程&#xff1a;留言找我要即可 2. postman发送get请求 import pymysql from flask import Flask,request# 这里是mysql的基本连接信息 c…

excel行转列

1.选中要转的内容&#xff0c;ctrlc 2.选择对应的大小&#xff0c;右击&#xff0c;点转置 3.ok

观察者模式——对象间的联动

1、简介 1.1、概述 在软件系统中&#xff0c;有些对象之间也存在类似交通信号灯和汽车之间的关系。一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变&#xff0c;它们之间将产生联动&#xff0c;正所谓“触一而牵百发”。为了更好地描述对象之间存在的这种一…