Semaphore实现原理全面解析

news2024/10/5 18:24:07

简介

Semaphore(信号量)是一个同步工具类,通过Semaphore可以控制同时访问共享资源的线程个数。

应用场景

Semaphore的主要应用场景:

  • 资源并发控制:Semaphore可以限制对资源的并发访问。如:管理数据库连接池或线程池中的资源。
  • 控制并发线程数:Semaphore可以控制同时执行的线程数量。如:控制同时访问某个接口的请求数量。
  • 实现互斥锁:Semaphore可以通过设置参数permits(信号量数量)的值为1来实现互斥锁的功能,保证同一时间只有一个线程可以访问临界区。
  • 控制任务流量:Semaphore可以限制任务的执行速率。如:控制某个任务在单位时间内的执行次数。

实现原理

在分析Semaphore的实现原理之前,先介绍一下信号量模型。

信号量模型

信号量模型是一个通用模型(即:与语言无关)。它主要由一个计数器、一个等待队列和三个方法组成,其中计数器和等待队列对外不可见,只能通过信号量模型提供的三个方法来访问它们。

信号量模型整体结构,如图所示:

图中:

  • init()方法:设置计数器的初始值。

  • down()方法:计数器的值-1,并根据当前计数器的值进行判断:

    • 如果当前计数器的值小于0,则阻塞当前线程(即:没有获得信号量)。
    • 如果当前计数器的值大于等于0,则执行当前线程(即:获得信号量)。
  • up()方法:计数器的值+1,并根据当前计数器的值进行判断:

    • 如果当前计数器的值小于等于0,说明释放信号量之前等待队列中存在处于阻塞状态的线程,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

注意:以上三个方法均是原子性操作。

整体流程

在Java中,信号量模型是通过Semaphore同步工具类实现的。整体流程如图所示:

处理流程:

  • 1)某个线程尝试获取信号量,将当前可用的信号量数量-本次需要的信号量数得到当前最新的信号量数:

    • 如果当前最新的信号量数大于等于0且通过CAS的方式更新当前最新的信号量数成功,则表示获取信号量成功,执行业务处理。
    • 如果当前最新的信号量数小于0,则表示获取信号量失败,将当前线程封装成Node节点追加到等待队列的末尾,进入阻塞,等待被唤醒。
  • 2)业务处理完成,释放信号量,将当前信号量数+本次释放的信号量数得到当前最新的信号量数。如果此时等待队列中存在阻塞的线程,则唤醒等待队列中阻塞的线程。

内部结构

Semaphore类结构定义,如图所示:

可以看到,Semaphore底层是基于AQS来实现。其中:

  • AbstractQueuedSynchronizer:抽象队列同步器(简称AQS)。
  • Sync:Semaphore的静态内部类,用于实现Semaphore的公共同步逻辑。
  • FairSync:Semaphore的静态内部类,继承了Sync,用于实现公平模式。
  • NonfairSync:Semaphore的静态内部类,继承了Sync,用于实现非公平模式。

源码如下:

public class Semaphore implements java.io.Serializable {
    // 同步变量,类型为Sync
    private final Sync sync;
    /**
     * Sync:Semaphore的静态内部类,它继承了AbstractQueuedSynchronizer,用于实现Semaphore的同步逻辑
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;
        // 初始化信号量数量
        Sync(int permits) {
            setState(permits);
        }
        // 获取当前信号量数量
        final int getPermits() {
            return getState();
        }
        /**
         * 以非公平模式尝试获取信号量
         */
        final int nonfairTryAcquireShared(int acquires) {
            // 自旋
            for (;;) {
                // 获取当前可用的信号量数量
                int available = getState();
                // 获取剩余信号量数量(当前可用的信号量数量-本次需要的信号量数量)
                int remaining = available - acquires;
                // 如果剩余信号量数量小于0或者剩余信号量数量大于等于0且更新剩余信号量数量成功,则返回当前剩余信号量数量
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
        /**
         * 尝试释放信号量
         */
        protected final boolean tryReleaseShared(int releases) {
            // 自旋
            for (;;) {
                // 获取当前信号量数量
                int current = getState();
                // 当前信号量数量+本次释放的信号量数量
                int next = current + releases;
                // 当前信号量数量超过int类型的最大值(即:溢出)
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                // 当前信号量数量未超过int类型的最大值且更新信号量数量成功,则返回释放信号量成功
                if (compareAndSetState(current, next))
                    return true;
            }
        }
        // 扣减指定数量的信号量
        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }
        // 清空并返回当前信号量数量
        final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }
    /**
     * NonfairSync:Semaphore的静态内部类,它继承了Sync,用于实现非公平模式
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;
        NonfairSync(int permits) {
            super(permits);
        }
        // 以非公平模式尝试获取信号量
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }
    /**
     * FairSync:Semaphore的静态内部类,它继承了Sync,用于实现公平模式
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;
        FairSync(int permits) {
            super(permits);
        }
        // 以公平模式尝试获取信号量
        protected int tryAcquireShared(int acquires) {
            // 自旋
            for (;;) {
                /**
                 * 当前线程对应的节点不是头节点的下一个节点或者当前线程不是持有信号量的线程,则获取信号量失败
                 * 其中:
                 *   当前线程对应的节点不是头节点的下一个节点,则获取信号量失败,体现的是公平原则(即:先到先得)
                 *   当前线程不是当前持有信号量的线程,则获取信号量失败,体现的是可重入
                 */
                if (hasQueuedPredecessors())
                    return -1;
                // 获取当前可用的信号量数量
                int available = getState();
                // 获取剩余信号量数量(当前可用的信号量数量-本次需要的信号量数量)
                int remaining = available - acquires;
                // 如果剩余信号量数量小于0或者剩余信号量数量大于等于0且更新剩余信号量数量成功,则返回当前剩余信号量数量
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
}

构造函数

Semaphore的构造函数:

/**
 * 构造函数1
 * permits:信号量数量,该参数值可能为负数,permits为负数时,必须要释放信号量后其他线程才能获取信号量
 */
public Semaphore(int permits) {
    // 默认为非公平模式
    sync = new NonfairSync(permits);
}
/**
 * 构造函数2
 * permits:信号量数量
 * fair:true-公平模式,false-非公平模式
 */
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

其中:

  • 公平模式:获取信号量时按照先后顺序进行分配,保证等待最久的线程能够优先获得信号量。
  • 非公平锁模式:获取信号量时不保证等待最久的线程能够优先获得信号量,而是根据系统调度算法选择合适的线程获取信号量。

核心方法

Semaphore的常用方法:

// 获取一个信号量
public void acquire() throws InterruptedException {...}
// 获取指定数量的信号量
public void acquire(int permits) throws InterruptedException {...}
// 获取一个信号量(忽略中断)
public void acquireUninterruptibly() {...}
// 获取指定数量的信号量(忽略中断)
public void acquireUninterruptibly(int permits) {...}
// 尝试获取一个信号量
public boolean tryAcquire() {...}
// 尝试在指定时间内获取一个信号量
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {...}
// 尝试获取指定数量的信号量
public boolean tryAcquire(int permits) {...}
// 尝试在指定时间内获取指定数量的信号量
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException {...}
// 释放一个信号量
public void release() {...}
// 释放指定数量的信号量
public void release(int permits) {...}
// 判断等待队列中是否存在阻塞的线程
public final boolean hasQueuedThreads() {...}
// 获取等待队列中阻塞的线程数量
public final int getQueueLength() {...}
// 获取等待队列中阻塞的线程集合
protected Collection<Thread> getQueuedThreads() {...}
// 获取可用的信号量数量
public int availablePermits() {...}
// 清空并返回当前信号量数量
public int drainPermits() {...}

其中,最核心的是acquire()方法和release()方法。

acquire方法

Semaphore通过调用Semaphore#acquire()方法获取一个信号量:

  • 如果(当前可用的信号量数量-1)大于等于0且通过CAS的方式更新当前最新的信号量数成功,则表示获取信号量成功,执行业务处理。
  • 如果(当前可用的信号量数量-1)小于0,则表示获取信号量失败,将当前线程封装成Node节点添加到等待队列中,进入阻塞,等待被唤醒,如果线程阻塞过程中被其他线程中断,则抛出InterruptedException异常。

Semaphore#acquire()方法源码解析:

// java.util.concurrent.Semaphore#acquire()
// 获取一个信号量
public void acquire() throws InterruptedException {
    // 以共享模式获取一个信号量
    sync.acquireSharedInterruptibly(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
// 以共享模式获取一个信号量(AQS)
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试以共享模式获取一个信号量(由继承AbstractQueuedSynchronizer类的Semaphore.Sync类实现)
    if (tryAcquireShared(arg) < 0)
        // 如果获取信号量失败(即:当前可用的信号量数量-1<0),则将当前线程封装成Node节点添加到等待队列中
        doAcquireSharedInterruptibly(arg);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireSharedInterruptibly
// 将当前线程封装成Node节点追加到等待队列的末尾,等待被唤醒(即:进入阻塞)
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 将封装当前线程的Node节点追加到等待队列的末尾(保证追加成功)
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 自旋
        for (;;) {
            // 获取Node节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点,则再次尝试获取信号量
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 设置当前节点为head节点,如果存在剩余资源,则唤醒下一个相邻的后继节点(即:向后传播)
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果前驱节点不是头节点或者获取信号量失败,则逆序遍历等待队列,找到可以唤醒自己的节点
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 将自己挂起(即:阻塞)
                parkAndCheckInterrupt())
                // 如果阻塞的线程被其他线程中断,则抛出InterruptedException异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

release方法

Semaphore通过调用Semaphore#release()方法释放一个信号量:

  • 如果(当前可用的信号量数量+1)未超过int类型的最大值,则释放信号量成功。如果此时等待队列中存在阻塞的线程,则唤醒等待队列中的阻塞的线程。
  • 如果(当前可用的信号量数量+1)超过int类型的最大值,则释放信号量失败,抛出"Maximum permit count exceeded"异常信息。

Semaphore#release()方法源码解析:

// java.util.concurrent.Semaphore#release()
// 释放一个信号量
public void release() {
    // 以共享模式释放一个信号量
    sync.releaseShared(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
// 以共享模式获取一个信号量(AQS)
public final boolean releaseShared(int arg) {
    // 尝试释放一个信号量(由继承AbstractQueuedSynchronizer类的Semaphore.Sync类实现)
    if (tryReleaseShared(arg)) {
        // 释放信号量成功,唤醒后继节点
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared()方法源码解析请移步主页查阅->「一文搞懂」AQS(抽象队列同步器)实现原理及源码解析。

使用示例

假设某个停车场有5个停车位,有8辆汽车想要进入停车场停车。

Semaphore示例代码:

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Semaphore;

/**
 * @program: xxkfz-study
 * @ClassName MyTest.java
 * @author: xxkfz
 * @create: 2024-02-24 21:18
 * @description:
 **/
@Slf4j
public class SemaphoreExample {

    public static void main(String[] args) {
        // 创建信号量,初始化信号量数为5(即:5个停车位)
        Semaphore semaphore = new Semaphore(2, true);
        // 创建8个线程,模拟8辆汽车进入停车场
        for (int i = 0; i < 8; i++) {
            int n = i;
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    try {
                        // 获取信号量
                        semaphore.acquire();
                        long time = (long) (Math.random() * 10 + n);
                        log.info(Thread.currentThread().getName() + "进入停车场,停车时间:{}秒", time);
                        // 模拟停车时长
                        Thread.sleep(time * 1000);
                    } finally {
                        log.info(Thread.currentThread().getName() + "离开停车场");
                        // 释放信号量
                        semaphore.release();
                    }
                }
            }, "第" + n + "号汽车").start();
        }
    }

}

执行结果:

10:04:45.249 [第0号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第0号汽车进入停车场,停车时间:7秒
10:04:45.249 [第1号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第1号汽车进入停车场,停车时间:3秒
10:04:48.264 [第1号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第1号汽车离开停车场
10:04:48.264 [第2号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第2号汽车进入停车场,停车时间:7秒
10:04:52.258 [第0号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第0号汽车离开停车场
10:04:52.258 [第6号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第6号汽车进入停车场,停车时间:14秒
10:04:55.278 [第2号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第2号汽车离开停车场
10:04:55.278 [第7号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第7号汽车进入停车场,停车时间:9秒
10:05:04.286 [第7号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第7号汽车离开停车场
10:05:04.286 [第5号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第5号汽车进入停车场,停车时间:13秒
10:05:06.271 [第6号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第6号汽车离开停车场
10:05:06.271 [第4号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第4号汽车进入停车场,停车时间:13秒
10:05:17.290 [第5号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第5号汽车离开停车场
10:05:17.290 [第3号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第3号汽车进入停车场,停车时间:8秒
10:05:19.273 [第4号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第4号汽车离开停车场
10:05:25.292 [第3号汽车] INFO com.xxkfz.simplememory.SemaphoreExample - 第3号汽车离开停车场

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

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

相关文章

蛇形矩阵2

题目描述 把数1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;…&#xff0c;按照“蛇形2”放入N*N矩阵的左三角形中&#xff0c;输出结果。 下面是N6的蛇形2的图示 输入格式 第一行1个正整数&#xff1a;N&#xff0c;范围在[1,100]。 输出格式 N行&a…

HuggingFists系统功能介绍(3)--流程管理

流程管理 下面我们进入流程管理。流程管理用于定义及管理我们采用低代码方式开发的各种ETL以及数据分析流程。 流程列表 界面的左侧部分为流程的分组树&#xff0c;当定义的流程逐渐变多或者多人协同使用系统时&#xff0c;可以通过分组对流程进行更有序的管理。点中分组&#…

C#与VisionPro联合开发——TCP/IP通信

TCP/IP&#xff08;传输控制协议/互联网协议&#xff09;是一组用于在网络上进行通信的通信协议。它是互联网和许多局域网的基础&#xff0c;为计算机之间的数据传输提供了可靠性、有序性和错误检测。在软件开发中&#xff0c;TCP/IP 通信通常用于实现网络应用程序之间的数据交…

预训练-微调范式在人工智能领域的深远影响

预训练-微调范式的出现是人工智能领域的一大里程碑&#xff0c;它深刻改变了深度学习模型的训练方式和应用模式&#xff0c;并对整个行业产生了多方面的深远影响&#xff1a; 数据效率提升&#xff1a; 通过在大规模无标注数据上进行预训练&#xff0c;模型能够学习到丰富的语言…

Python算法题集_实现 Trie [前缀树]

Python算法题集_实现 Trie [前缀树] 题208&#xff1a;实现 Trie (前缀树)1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【定义数据类默认字典】2) 改进版一【初始化字典无额外类】3) 改进版二【字典保存结尾信息无额外类】 4. 最优算法5. 相关…

实战一个 Jenkins 构建 CI/CD流水线 的简单配置过程哈

引言&#xff1a;上一期我们讲述了gitlabCI/CD工具的介绍&#xff0c;工具之争&#xff0c;本期我们介绍Jenkins CI/CD 目录 一、Jenkins介绍 1、Jenkins概念 2、Jenkins目的 3、特性 4、产品发布流程 二、安装Jenkins 1、安装JDK 2、安装Jenkins 1、上传压缩包 2、…

Python入门必学:单引号、双引号与三引号的差异与应用

Python入门必学&#xff1a;单引号、双引号与三引号的差异与应用 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得…

神经网络系列---激活函数

文章目录 激活函数Sigmoid 激活函数Tanh激活函数ReLU激活函数Leaky ReLU激活函数Parametric ReLU激活函数 &#xff08;自适应Leaky ReLU激活函数&#xff09;ELU激活函数SeLU激活函数Softmax 激活函数Swish 激活函数Maxout激活函数Softplus激活函数 激活函数 一般来说&#xf…

Tomcat线程池原理(上篇:初始化原理)

文章目录 前言正文一、从启动脚本开始分析二、ProtocolHandler 的启动原理三、AbstractEndPoint 的启动原理四、创建默认线程池五、参数配置原理5.1 常规的参数配置5.2 自定义线程池5.3 测试自定义线程 前言 在Java Web的开发过程中&#xff0c;Tomcat常用的web容器。SpringBo…

SpringBoot -【BeanPostProcessor】基础使用及应用场景

BeanPostProcessor应用与优化 1. 引言 在现代软件开发中&#xff0c;企业开发面临着越来越复杂的系统架构和业务需求。随着项目规模的扩大和技术栈的增多&#xff0c;需要更高效的工具来应对这些挑战&#xff0c;并确保代码的可维护性和扩展性。 在这样的背景下&#xff0c;Be…

Linux之项目部署与发布

目录 一、Nginx配置安装&#xff08;自启动&#xff09; 1.一键安装4个依赖 2. 下载并解压安装包 3. 安装Nginx 4. 启动 nginx 服务 5. 对外开放端口 6. 配置开机自启动 7.修改/etc/rc.d/rc.local的权限 二、后端部署tomcat负载均衡 1. 准备2个tomcat 2. 修改端口 3…

随机分布模型

目录 前言 一、离散型随机变量 1.1 0-1分布 1.2 二项分布 1.3 帕斯卡分布 1.4 几何分布 1.5 超几何分布 1.6 泊松分布 二、连续型随机变量 2.1 均匀分布 2.2 指数分布 2.3 高斯分布/正态分布 2.4 分布&#xff08;抽样分布&#xff09; 2.5 t分布&#xff08;抽样…

Vue局部注册组件实现组件化登录注册

Vue局部注册组件实现组件化登录注册 一、效果二、代码1、index.js2、App.vue3、首页4、登录&#xff08;注册同理&#xff09; 一、效果 注意我这里使用了element组件 二、代码 1、index.js import Vue from vue import VueRouter from vue-router import Login from ../vie…

迷你世界之建筑生成球体

local x0,y0,z00,30,0--起点坐标 local dx,dy,dz60,60,60--外切长方体横纵竖长度 local count,all0,dx*dy*dz--计数&#xff0c;总数 local m,k10000,0--单次生成方块数&#xff0c;无用循环值 local x,y,z0,0,0--当前坐标 local demath.random(2,19)/2 local id600--方块…

【监督学习之逻辑回归】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 简述概要知识图谱1.什么是逻辑回归&#xff1f;2.逻辑回归有哪些应用&#xff1f;3.回归分析如何工作&#xff1f;4.逻辑回归模型如何工作&#xff1f;5.逻辑回归分析有哪些类型&#xff1f;6.逻辑回归与其他机器学习技术相…

APP攻防-实战拿下某seseAPPSpringboot未授权HeapDump提取OSS利用

知识点 1、APK-抓包 2、资产信息收集 3、SpringBoot-漏洞利用 4、自动化工具 5、HeapDump-分析提取 6、AccessKEY-利用后续 演示案例&#xff1a; 1、APK-抓包 2、资产信息收集 3、SpringBoot-漏洞利用 SpringBoot漏洞利用&#xff1a; https://github.com/LandGrey/Spring…

K8S—集群调度

目录 前言 一 List-Watch 1.1 list-watch概述 1.2 list-watch工作机制 二 集群调度 2.1 调度过程 2.2 Predicate 和 Priorities 的常见算法和优先级选项 2.3 调度方式 三 亲和性 3.1 节点亲和性 3.2 Pod 亲和性 3.3 键值运算关系 3.4 Pod亲和性与反亲和性 3.5 示例…

【小记】简历 Tips

基本信息 真实诚信 教育经历&#xff08;院校信息&#xff09;、成绩、在校经历&#xff1b; 公司、项目经历 注意事项 逻辑清晰、重点突出、岗位JD完全吻合&#xff1b; 简洁&#xff08;一页纸简历、顺序从上到下&#xff09; 做了什么&#xff08;时间顺序&#xff09; 成果…

Escalate_Linux-环境变量劫持提权(5)

环境变量劫持提权 在Shll输入命令时&#xff0c;Shel会按PAH环境变量中的路径依次搜索命令&#xff0c;若是存在同名的命令&#xff0c;则执行最先找到的&#xff0c;若是PATH中加入了当前目录&#xff0c;也就是“”这个符号&#xff0c;则可能会被黑客利用&#xff0c;例如在…

冯诺依曼体系结构 计算机组成的金字塔

01 冯诺依曼体系结构&#xff1a;计算机组成的金字塔 学习计算机组成原理&#xff0c;到底是在学些什么呢&#xff1f;这个事儿&#xff0c;一两句话还真说不清楚。不过没关系&#xff0c;我们先从“装电脑”这个看起来没有什么技术含量的事情说起&#xff0c;来弄清楚计算机到…