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

目录​​​​​​​

前言

上下文切换

多线程一定快吗?

如何减少上下文切换

死锁

资源限制的挑战


前言

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

上下文切换

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

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

多线程一定快吗?

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

public class ConcurrencyTest {

    private static final long count = 1000 * 1000 * 1000L;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            int a = 0;
            for (int i = 0; i < count; i++) {
                a += 1;
            }
        });

        int b = 0;
        for (int i = 0; i < count; i++) {
            b += 1;
        }
        thread1.join();
        long costTime = System.currentTimeMillis() - start;
        System.out.println("多线程执行时间: " + costTime);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (int i = 0; i < count; i++) {
            a += 1;
        }

        int b = 0;
        for (int i = 0; i < count; i++) {
            b += 1;
        }
        long costTime = System.currentTimeMillis() - start;
        System.out.println("单线程执行时间: " + costTime);
    }

}
循环次数 串行执行耗时/ms 并发执行耗时 并发与串行对比
10万 0 49 串行快
100万 10 50 串行快
1000万 15 50 串行快
1亿 80 90 差不多
10亿 889 500 并发快

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

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

如何减少上下文切换

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

死锁

出现死锁的四个必要条件

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

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

public class DeadLockTest {

    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) throws InterruptedException {
        new DeadLockTest().deadLock();
    }

    private void deadLock() throws InterruptedException {
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("ThreadA get lockA");
                // 方便更容易出现死锁
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("ThreadA get lockB");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("ThreadB get lockB");
                // 方便更容易出现死锁
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println("ThreadB get lockA");
                }
            }
        }).start();

    }

}

《Java并发编程的艺术》读书笔记 - 第一章 - 并发编程的挑战_第1张图片

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

《Java并发编程的艺术》读书笔记 - 第一章 - 并发编程的挑战_第2张图片

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

避免死锁的4个常见方法

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

资源限制的挑战

什么是资源限制?

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

资源限制引发的问题

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

如何解决资源限制的问题

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

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

你可能感兴趣的:(Java并发编程与实战落地,java,Java并发,并发编程,读书笔记,多线程)