• 《Java并发编程的艺术》读书笔记 - 第一章 - 并发编程的挑战


    目录

    前言

    上下文切换

    多线程一定快吗?

    如何减少上下文切换

    死锁

    资源限制的挑战


    前言

            并发编程的目的是为了让程序运行得更快,但并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,这会面临非常多的挑战,比如上下文切换、死锁、以及受限于硬件和软件的资源等多种问题。

    上下文切换

            即便是单核处理器它也支持多线程执行代码,CPU是通过给每个线程分配时间片来实现的多线程执行代码机制,这个时间片约为几十毫秒(ms),人一般感知不到多个线程来回切换的动作,这让我们产生一种程序是多线程运行的错觉。

            所谓上下文切换,实际上就是线程执行任务从保存到再加载的一次过程。每个线程当前时间片执行结束后会保存当前任务的状态,以便下次切回这个任务时可以方便的再加载回原状态。例如我们在翻阅英文文献时遇到了生僻的词汇,在翻译软件查找结果之前我们应当记住当前文献的阅读位置,以免找到结果后返回到文献时不清楚生僻词的位置。这样的切换会影响阅读效率,同样上下文切换也会影响多线程的执行速度。

    多线程一定快吗?

    用下面一段程序测试一下多线程和单线程对于累加变量的耗时统计

    1. public class ConcurrencyTest {
    2. private static final long count = 1000 * 1000 * 1000L;
    3. public static void main(String[] args) throws InterruptedException {
    4. concurrency();
    5. serial();
    6. }
    7. private static void concurrency() throws InterruptedException {
    8. long start = System.currentTimeMillis();
    9. Thread thread1 = new Thread(() -> {
    10. int a = 0;
    11. for (int i = 0; i < count; i++) {
    12. a += 1;
    13. }
    14. });
    15. int b = 0;
    16. for (int i = 0; i < count; i++) {
    17. b += 1;
    18. }
    19. thread1.join();
    20. long costTime = System.currentTimeMillis() - start;
    21. System.out.println("多线程执行时间: " + costTime);
    22. }
    23. private static void serial() {
    24. long start = System.currentTimeMillis();
    25. int a = 0;
    26. for (int i = 0; i < count; i++) {
    27. a += 1;
    28. }
    29. int b = 0;
    30. for (int i = 0; i < count; i++) {
    31. b += 1;
    32. }
    33. long costTime = System.currentTimeMillis() - start;
    34. System.out.println("单线程执行时间: " + costTime);
    35. }
    36. }
    循环次数串行执行耗时/ms并发执行耗时并发与串行对比
    10万049串行快
    100万1050串行快
    1000万1550串行快
    1亿8090差不多
    10亿889500并发快

    注:每个人的电脑性能都有差异,上述只是实验数据。

    通过数据可以发现当并发执行累加操作不超过亿级次时,速度会比串行执行要慢,这是因为线程有创建和上下文切换的开销。

    如何减少上下文切换

    • 无锁并发编程:如将数据的ID按照Hash算法取模分段,让不同的线程处理不同段的数据
    • CAS算法:Java的Atomic包使用此算法来更新数据,不需要加锁
    • 使用最少线程:避免创建大量的线程导致资源浪费
    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换 

    死锁

    出现死锁的四个必要条件

    • 互斥:一个资源每次只能被一个进程使用;
    • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
    • 不剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺;
    • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系; 

    下面来看一段出现死锁的代码:

    1. public class DeadLockTest {
    2. private static final Object lockA = new Object();
    3. private static final Object lockB = new Object();
    4. public static void main(String[] args) throws InterruptedException {
    5. new DeadLockTest().deadLock();
    6. }
    7. private void deadLock() throws InterruptedException {
    8. new Thread(() -> {
    9. synchronized (lockA) {
    10. System.out.println("ThreadA get lockA");
    11. // 方便更容易出现死锁
    12. try {
    13. Thread.sleep(500);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. synchronized (lockB) {
    18. System.out.println("ThreadA get lockB");
    19. }
    20. }
    21. }).start();
    22. new Thread(() -> {
    23. synchronized (lockB) {
    24. System.out.println("ThreadB get lockB");
    25. // 方便更容易出现死锁
    26. try {
    27. Thread.sleep(500);
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. }
    31. synchronized (lockA) {
    32. System.out.println("ThreadB get lockA");
    33. }
    34. }
    35. }).start();
    36. }
    37. }

    程序一直在运行没有停止,可以利用jps + jstack命令检测是否真的出现了死锁

    在实际工作中一定要避免出现因为异常情况引发的死锁问题。

    避免死锁的4个常见方法

    • 避免一个线程同时获取多把锁
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    • 尝试使用定时锁,使用lock.tryLock(timeout) 来替代使用内部锁机制
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况 

    资源限制的挑战

    什么是资源限制?

    资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s,系统启动10个线程下载资源,下载速度并不会编程10Mb/s,所以在并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传 / 下载速度,硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。

    资源限制引发的问题

    如果计算机只有一个单核处理器,那么你在此基础上并发编程只会降低程序运行的效率,你期望的并发执行实际上是串行执行。其中增加了大量上下文切换和资源调度的时间。 

    如何解决资源限制的问题

    硬件资源限制:考虑搭建服务端集群,利用“数据ID % 机器数”  得到机器编号,然后由对应编号的机器处理这个任务。

    软件资源限制:尽可能的考虑使用资源池进行资源复用,常见的例如线程池、数据库连接池等都是使用这种方式。

  • 相关阅读:
    数据结构学习笔记(第四章 串)
    记录linux清理空间的步骤
    C语言学习-数组(4)
    多商户商城系统功能拆解29讲-平台端营销-会员签到
    Python语音识别处理详解
    OKR,为什么我喜欢他们
    HarmonyOS开发之一——环境安装和HelloWorld
    0基础可以转行编程行业么
    IDA、X32dbg逆向分析易语言程序窗口标题、宽度、高度
    guava之RateLimiter
  • 原文地址:https://blog.csdn.net/shuttlepro/article/details/127705381