目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

并行的世界

并发简史

 早期计算机(不包含操作系统),只能从头到尾执行一个程序,并且执行的程序能访问计算机中的所有资源。
 操作系统的出现使得计算机每次能运行多个程序,并且不同程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存、文件句柄以及安全证书等。计算机中加入操作系统来实现多个程序的同时执行,主要是基于以下原因:

  • 资源利用率(同步等待造成的资源浪费)
  • 公平性(通过时间分片实现用户和程序共享计算机资源)
  • 便利性(多程序开发可以降低单个程序的开发的复杂度)

串行与并行的区别

 好处:可以缩短整个流程的时间

  • 串行(5):洗茶具 => 打水 => 烧水(同步等待) => 等水开 => 冲茶
  • 并行(4):打水 => 烧水同时洗茶具 => 水开 => 冲茶

可怕的现实:摩尔定律的失效

 摩尔定律:当价格不变时,集成电路上可容纳的元器件的数目,约每隔 18-24 个月便会增加一倍,性能也将提升一倍。这一定律揭示了信息技术进步的速度。 ​
 然而,在 2004 年,Intel 的 4GHz 芯片宣布推迟到 2005 年,然后再 2004 年的秋季,又再次宣布彻底取消 4GHz 计划。
 究其根本,硅电路很有可能已经走到的尽头,制造工艺已经精确到了纳米,1 纳米是 10 的负 9 次方米,也就是十亿分之一米。就目前科技水平而言,如果无法在物质分子层面以下进行工作,那么 4GHz 已经很接近理论的极限值了。因为即使一个水分子,他的直径也只有 0.4 纳米。
顶级计算机科学家唐纳德·尔文·克努斯(Donald Ervin Knuth)说过:在我看来,这种现象(并发)或多或少是由于硬件设计者已经无计可施了导致的,他们将摩尔定律失效的责任推给软件开发者。

光明或黑暗

 根据唐纳德的观点,摩尔定律本应该由硬件开发人员维护。但是,很不幸,硬件工程受限于当代科学水平,似乎已经无计可施。然而为了继续保持性能的高速发展,硬件工程师破天荒地想出了将多个 CPU 内核塞进一个 CPU 里的奇妙想法。由此,并行计算就被非常自然地推广开来,随之而来的问题也层出不穷,软件开发人员的黑暗时期也随之到来。简化的硬件设计方案必然带来软件设计的复杂性。软件开发人员正在为硬件工程师无法完成的工作买单。

并发编程的适用场景

  • 任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况。
  • 任务执行时间过长,可以划分为分工明确的子任务:比如分段下载。
  • 任务间断性执行:日志打印。
  • 任务本身需要协作执行:比如生产者消费者问题。

并行计算相关概念

同步(Synchronous)和异步(Asynchronous)
 同步和异步通常用来形容一次方法的调用。

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步调用则立刻返回,调用者就可以继续后续的操作。

并发(Concurrency)和并行(Parallelism)
 他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。

  • 并发侧重多任务交替执行,而多个任务之间有可能还是串行的。
  • 并行是真正意义上的“同步执行”。

临界区
 临界区用来表示一种共享资源,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程想使用临界区就必须等待。

阻塞(Blocking)和非阻塞(Non-Blocking)
 阻塞和非阻塞通常用来形容多线程间的相互影响。

  • 阻塞:比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作,一直处于等待、线程挂起的状态。
  • 非阻塞:非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断前向执行。

并发编程的挑战之频繁的上下文切换

 CPU 为线程分配时间片,时间片非常短(毫秒级别),CPU 不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。
但是上下文的频繁切换,会带来一定的性能开销。并且上下文切换本身是没有意义的(零生产力),如何减少上下文切换的开销?

  • 无锁并发编程 :多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS:Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态(频繁的上下文切换只会操作 CPU 时间片的浪费)。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。--GO

并发编程的挑战之死锁

死锁示例代码

/**
 * 死锁Demo
 */
public class DeadLockDemo {

    private static final Object HAIR_A = new Object();

    private static final Object HAIR_B = new Object();

    public static void main(String[] args) {

        new Thread(()->{
            synchronized (HAIR_A) {
                //休眠一段时间
                try {
                    Thread.sleep(50L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (HAIR_B) { //A获取不到B的锁,因为B一直不释放自己持有的锁
                    System.out.println("A成功的抓住B的头发");
                }
            }
        }).start();

        new Thread(()->{
            synchronized (HAIR_B) { 
                synchronized (HAIR_A) {  //B获取不到A的锁,因为A一直不释放自己持有的锁
                    System.out.println("B成功抓到A的头发");
                }
            }
        }).start();
    }
}

如何排查死锁方式一

C:\Users\soulboy>jps
18160 Launcher
18148 Jps
16108 DeadLockDemo
18108

C:\Users\soulboy>jstack 16108
Found one Java-level deadlock:

=============================
"Thread-1":
  waiting to lock monitor 0x000000001cd12168 (object 0x000000076b18a5f0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001cd13608 (object 0x000000076b18a600, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.xdclass.synopsis.DeadLockDemo.lambda$main$1(DeadLockDemo.java:28)
        - waiting to lock <0x000000076b18a5f0> (a java.lang.Object)
        - locked <0x000000076b18a600> (a java.lang.Object)
        at com.xdclass.synopsis.DeadLockDemo$$Lambda$2/1831932724.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
"Thread-0":
        at com.xdclass.synopsis.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
        - waiting to lock <0x000000076b18a600> (a java.lang.Object)
        - locked <0x000000076b18a5f0> (a java.lang.Object)
        at com.xdclass.synopsis.DeadLockDemo$$Lambda$1/990368553.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.

如何排查死锁方式二

C:\Users\soulboy>jconsole

jconsole

并发编程的挑战之线程安全

线程安全问题的原因
 不同于死锁,线程安全比较难以定位。线程安全问题通常都是因为多个线程在操作共享的数据产生的。

线程不安全代码示例

import java.util.concurrent.CountDownLatch;

/**
 * 线程不安全操作代码实例
 */
public class UnSafeThread {

    private static int num = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    /**
     * 每次调用对num进行++操作
     */
    public static void  inCreate() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{    //创建10个线程
                for (int j = 0; j < 100; j++) { //每个线程调用inCreate方法100次
                    inCreate();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //每个线程执行完成之后,调用countDownLatch
                countDownLatch.countDown();
            }).start();
        }

        while (true) {
            if (countDownLatch.getCount() == 0) { //10个线程都执行完毕才进行打印num
                System.out.println(num);
                break;
            }
        }
    }
}

控制台输出不符合预期的值:1000

# 每次执行结果都不尽相同,都无法满足预期值
826
799

作者:Soulboy