多线程就一定快吗?

        并发编程的目的其实就是为了让程序运行的更快, 但是并不是只要是多线程, 就一定能让程序最大限度的进行并发执行. 最常见的就是我们再编程中遇到的各种问题, 例如死锁等, 接下来我会从几个角度来解析相关的问题. 


 

目录

单核CPU的多线程并发

线程的上下文切换

死锁(Deadlock)

有限的资源竞争


单核CPU的多线程并发

        在早期, 大多数linux设备都是1c的也就是单核CPU, 这是很常见的设备, 你也可以在阿里云这样的云服务解决平台发现很多其他核心数的设备: 

        例如我自己就有一台2c的服务器: 

        线程是操作系统 调度和执行的基本单位, 同一时间, 单核CPU只能运行一个线程, 那其他线程就只能等待, 具体该怎么等待, 由具体的调度算法实现来决定. 

        具体的调度算法有(操作系统的知识, 随便过一遍就行)

  1. 先来先服务(FCFS)

    • 线程按照到达就绪队列的先后顺序进行调度
    • 这种算法简单且公平,但可能导致饥饿问题(即某些线程长时间得不到执行)
  2. 最短作业优先(SJF)

    • 选择估计执行时间最短的线程进行调度
    • 这种算法可以提高系统的吞吐量,但同样存在饥饿问题,且需要事先知道线程的估计执行时间
  3. 优先级调度

    • 根据线程的优先级进行调度。优先级高的线程优先获得CPU的执行权
    • 优先级调度算法可以灵活地满足不同线程的优先级需求,但可能导致低优先级线程的饥饿问题
  4. 时间片轮转法(Round Robin)

    • 将所有就绪线程按先来先服务的原则排成一个队列
    • 每次调度时,将CPU分配给队首线程,并令其执行一个时间片
    • 当执行的时间片用完时,由计时器发出时钟中断请求,调度程序便据此信号来停止该线程的执行,并将其送往就绪队列的末尾
    • 然后,再把处理机分配给就绪队列中新的队首线程,同时也让它执行一个时间片
    • 这种算法可以保证就绪队列中的所有线程在一给定的时间内均能获得一时间片的处理机执行时间
  5. 多级反馈队列调度(MLFQ)

    • 根据优先级将线程放入不同的就绪队列中
    • 每个队列都有自己的时间片长度,优先级越高的队列时间片越短
    • 如果一个线程在一个时间片内没有完成执行,则会被降低到下一个优先级较低的队列中
    • 这种算法可以灵活地调整线程的优先级,并减少饥饿问题的发生

        我们屏蔽掉调度的方式, 直接看调度带来的结果(显然无论是什么调度算法, 最终都会让一个线程下cpu, 然后让另外一个线程上cpu执行) , 既然要切换, 那不能说我每次切换完线程之后, 就需要重新执行之前任务吧. 

        就好比小明经常喜欢一心二用, 一边玩游戏还一边写作业, 这两种东西都需要大脑来思考, 假设小明刚看完题目, 就去开了一把王者荣耀, 玩完之后就又去写作业, 但是这个题目他已经看过了, 就不需要在看了, 直接做题就行. 因为这个题目他已经记在大脑了. 

        线程同样如此, 一个线程在执行一个从1累加到100的过程, 累加到60的时候就因为系统的调度不得不放弃cpu, 但是下次这个线程再次被加载到cpu的时候, 应该从60的时候继续累加, 而不是从新从1开始. 

        因此这个会有线程私有的程序计数器来单独记录. 

        这样看来, 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地
切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)

线程的上下文切换

        上述的累加到一半被卸载, 然后又获得调度的机会, 重新被加载到cpu的过程, 就被称为一次上下文切换.  但是上下文切换的过程, 需要额外的进行性能消耗: 

一般来说需要存储如下信息:

  • CPU寄存器:保存当前进程的指令位置、数据寄存器等状态信息
  • 程序计数器:记录当前进程执行到哪一条指令
  • 堆栈信息:保存当前进程的调用栈和局部变量等
  • 内核堆栈和寄存器:对于进程上下文切换,还需要保存和恢复内核态的堆栈和寄存器信息

        上下文切换是由操作系统直接支持的, 因此需要操作系统从用户态切换到核心态来执行,  同时恢复上下文内容需要各种加载, 因此比较耗时. 

        因此我们模拟一个Java方法, 来看单核的情况下的多线程快还是单线程快: 

public class Test{
    private static final long count = 10000L;
    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }
    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < count; i++) {
                a += 5;
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        thread.join();
        System.out.println("concurrency :" + time+"ms,b="+b);
    }
    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time+"ms,b="+b+",a="+a);
    }
}

        这个类中 concurrency (并行)来模拟多个线程执行操作, 一个线程执行count次++操作, main线程执行count次--操作, serial(串行)中 由main线程来完成这两个++和--操作.

         你可以多执行几次, 你会发现, 执行次数越多, 单线程反而比多线程更快. 

        下图是一个可以参考的结果: 

 

死锁(Deadlock)

        锁设置的不恰当, 也会产生不恰当的锁竞争问题, 从而导致性能下降, 说到死锁你可能会想起几个经典的案例, 例如最经典的银行家算法来解决死锁的问题:

此图来自 百度百科:很行家算法

还有经典的面试题: "a is lack of lock_b and b is lack of lock_a": 

        当两个线程或进程 a 和 b 分别缺少对方持有的锁(即 a 缺少 lock_b,而 b 缺少 lock_a)时,这种情况通常会导致死锁, 

        死锁是一个非常验证的BUG, 大多数情况是直接被定义为P0级别的. 

以下是其主要影响:

  1. 进程或线程无法继续执行
    • 处于死锁状态的进程或线程无法获得所需的资源,因此无法继续执行其任务。这可能导致应用程序或服务无法响应,甚至崩溃。
  2. 资源利用率降低
    • 死锁会导致系统资源(如CPU、内存、文件、数据库连接等)被长时间占用而无法释放,从而降低资源利用率。这可能导致系统性能下降,甚至无法处理新的请求。
  3. 系统崩溃
    • 在极端情况下,死锁可能导致整个系统崩溃。例如,在操作系统中,如果关键的系统进程因死锁而无法执行,那么整个系统可能会变得不稳定并崩溃。
  4. 用户体验受损
    • 在用户交互的应用程序中,死锁可能导致用户界面无响应或响应缓慢,从而严重影响用户体验。
  5. 数据不一致
    • 在数据库系统中,死锁可能导致事务无法完成,从而导致数据不一致。这可能需要额外的恢复措施来确保数据的完整性和一致性。
  6. 开发和维护成本增加
    • 死锁问题通常很难调试和修复,因为它们可能涉及多个进程或线程的复杂交互。这增加了开发和维护成本,并可能导致项目延期或交付质量下降。
  7. 产生多米诺骨牌效应
    • 一个进程出现死锁,有可能产生多米诺骨牌效应,导致其他进程也发生死锁,最终可能导致操作系统崩溃。

有限的资源竞争

        学过计算机网络的都知道,  如果自己家的网络出现了问题, 该怎么找原因: 

        假设小明家最近升级了光纤, 现在的网速理论是1000Mbps, 但是实测的网速值达到了500Mbps, 小明很生气去找运营商, 结果被告知去更换路由器和网线. 

        (此处的5g指的是5Gbps, 转化为MB就是640MBps_640MB/s, 注意这里是B_字节, 而不是b_比特bit, 几秒下载一个王者荣耀) 

        其中的原理就是可能是由于路由器产品年代久远, 接收和发送数据的速度远低于理论的1000Mbps, 假设为700Mbps, 那么用户接受的数据的最高网速也就会低于或等于700Mbps, 同理, 如果网线的传输效率为500Mbps, 那么就刚好印证了为什么小明家的网速只有理论的1000Mbps的一半了

        现在假设小明使用他家的pc进行下载, 此时小明刚好网上学习了一个Java多线程下载器的项目, 信心满满的去尝试使用多线程来提高下载速度, 结果发现, 速度并没有提升多少, 最大值刚好就是他家的1000Mbps的90%左右(假设他家其他设备占用10%, 并且小明主机的网线速率大于1000Mbps). 

         可以看到, 即使是多线程, 你的下载速度也快不起来的啊, 除此之外, 别的一些资源, 也会限制程序的执行速度, 例如cpu资源, 硬盘读写, 数据库连接等. 

        解决方案就是突破资源限制, 例如多核CPU并行计算, 使用集群扩展存储和计算能力, 突破限制, 使用更高的带宽

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值