关于多线程:并发开篇带你从0到1建立并发知识体系的基石

39次阅读

共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。

并发开篇——带你从 0 到 1 建设并发常识体系的基石

前言

在本篇文章当中次要跟大家介绍并发的基础知识,从最根本的问题登程层层深刻,帮忙大家理解并发常识,并且打好并发的根底,为前面深刻学习并发提供保障。本篇文章的篇章构造如下:

并发的需要

  • 咱们罕用的软件就可能会有这种需要,对于一种软件咱们可能有多重需要,程序可能一边在运行一边在后盾更新,因而在很多状况下对于一个过程或者一个工作来说可能想要同时执行两个不同的子工作,因而就须要在一个过程当中产生多个子线程,不同的线程执行不同的工作。
  • 当初的机器的 CPU 外围个数个别都有很多个,比方当初个别的电脑都会有 4 个 CPU,而每一个 CPU 在同一个时刻都能够执行一个工作,因而为了充分利用 CPU 的计算资源,咱们能够让这多个 CPU 同时执行不同的工作,让他们同时工作起来,而不是闲暇没有事可做。
  • 还有就是在科学计算和高性能计算畛域有这样的需要,比方矩阵计算,如果一个线程进行计算的话须要很长的工夫,那么咱们就可能应用多核的劣势,让多个 CPU 同时进行计算,这样一个计算工作的计算工夫就会比之前少很多,比方一个工作单线程的计算工夫为 24 小时,如果咱们有 24 个 CPU 外围,那么咱们的计算工作可能在 1 - 2 小时就计算实现了,能够节约十分多的工夫。

并发的根底概念

在并发当中最常见的两个概念就是过程和线程了,那什么是过程和线程呢?

  • 过程简略的说来就是一个程序的执行,比如说你再 windows 操作系统当中双击一个程序,在 linux 当中在命令行执行一条命令等等,就会产生一个过程,总之过程是一个独立的主体,他能够被操作系统调度和执行。
  • 而线程必须依赖过程执行,只有在过程当中能力产生线程,当初通常会将线程称为轻量级过程(Light Weight Process)。一个过程能够产生多个线程,二者多个线程之间共享过程当中的某些数据,比方全局数据区的数据,然而线程的本地数据是不进行共享的。

你可能会听过过程是资源分配的根本单位,这句话是怎么来的呢?在下面咱们曾经提到了线程必须依赖于过程而存在,在咱们启动一个程序的时候咱们就会开启一个过程,而这个过程会像操作系统申请资源,比方内存,磁盘和 CPU 等等,这就是为什么操作系统是申请资源的根本单位。

你可能也听过线程是操作系统调度的根本单位。那这又是为什么呢?首先你须要明确 CPU 是如何工作的,首先须要明确咱们的程序会被编译成一条条的指令,而这些指令会存在在内存当中,而 CPU 会从内存当中一一的取出这些指令,而后 CPU 进行指令的执行,而一个线程通常是执行一个函数,而这个函数也是会被编译成很多指令,因而这个线程也能够被 CPU 执行,因为线程能够被操作系统调度,将其放到 CPU 上进行执行,而且没有比线程更小的能够被 CPU 调度的单位了,因而说线程是操作系统调度的根本单位。

Java 实现并发

继承 Thread 类

public class ConcurrencyMethod1 extends Thread {

    @Override
    public void run() {// Thread.currentThread().getName() 失去以后正在执行的线程的名字
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {for (int i = 0; i < 5; i++) {
            // 新开启一个线程
            ConcurrencyMethod1 t = new ConcurrencyMethod1();
            t.start();// 启动这个线程}
    }
}
// 某次执行输入的后果(输入的程序不肯定)Thread-0
Thread-4
Thread-1
Thread-2
Thread-3

下面代码当中不同的线程须要失去 CPU 资源,在 CPU 当中被执行,而这些线程须要被操作系统调度,而后由操作系统放到不同的 CPU 上,最终输入不同的字符串。

应用匿名外部类实现 runnable 接口

public class ConcurrencyMethod2 extends Thread {public static void main(String[] args) {for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {// Thread.currentThread().getName() 失去以后正在执行的线程的名字
                    System.out.println(Thread.currentThread().getName());
                }
            });
            thread.start();}
    }
}
// 某次执行输入的后果(输入的程序不肯定)Thread-0
Thread-1
Thread-2
Thread-4
Thread-3

当然你也能够采纳 Lambda 函数去实现:

public class ConcurrencyMethod3 {public static void main(String[] args) {for (int i=0; i < 5; i++) {Thread thread = new Thread(() -> {System.out.println(Thread.currentThread().getName());
            });
            thread.start();}
    }
}
// 输入后果
Thread-0
Thread-1
Thread-2
Thread-4
Thread-3

其实还有一种 JDK 给咱们提供的办法去实现多线程,这个点咱们在后文当中会进行阐明。

了解主线程和 Join 函数

如果当初咱们有一个工作,子线程输入一下本人的线程的名字,在线程输入完本人的名字之后,主线程再输入字符串“线程执行实现”。

在实现下面的工作之前,首先咱们须要明确什么是主线程和子线程,所谓主线程就是在执行 Java 程序的时候不是通过 new Thread 操作这样显示的创立的线程。比方在咱们的非并发的程序当中,执行程序的线程就是主线程。

public class MainThread {public static void main(String[] args) {System.out.println("我是主线程");
    }
}

比方在下面的代码当中执行语句 System.out.println("我是主线程"); 的线程就是主线程。

public class MainThread {public static void main(String[] args) {
        // 上面这段代码是由主线程执行的
        // 主线程通过上面这段代码创立一个子线程
        Thread thread = new Thread(() -> {System.out.println("我是主线程创立的子线程");
        });
        // 这句代码也是主线程执行的
        // 次要意义就是主线程启动子线程
        thread.start();
        System.out.println("我是主线程");
    }
}

当初咱们再来看一下咱们之前的工作:

如果当初咱们有一个工作,子线程输入一下本人的线程的名字,在线程输入完本人的名字之后,主线程再输入字符串“线程执行实现”。

下面的工作很明确就是主线程在执行输入本人线程的名字的语句必之前,须期待子线程执行实现,而在 Java 线程当中给我提供了一种形式,帮忙咱们实现这一点,能够保障主线程的某段代码能够在子线程执行实现之后再执行。

public class MainThread {public static void main(String[] args) throws InterruptedException {
        // 上面这段代码是由主线程执行的
        // 主线程通过上面这段代码创立一个子线程
        Thread thread = new Thread(() -> {System.out.println(Thread.currentThread().getName());
        });
        // 这句代码也是主线程执行的
        // 次要意义就是主线程启动子线程
        thread.start();
        // 这句代码的含意就是阻塞主线程
        // 直到 thread 的 run 函数执行实现
        thread.join();
        System.out.println(Thread.currentThread().getName());
    }
}
// 输入后果
Thread-0
main

下面代码的执行流程大抵如下图所示:

咱们须要晓得的一点是 thread.join() 这条语句是主线程执行的,它的次要性能就是期待线程 thread 执行实现,只有 thread 执行实现之后主线程才会继续执行 thread.join() 前面的语句。

第一个并发工作——求 $x^2$ 的积分

接下来咱们用一个例子去具体领会并发带来的成果晋升。咱们的这个例子就是求函数的积分,咱们的函数为最简略的二次函数 $x^2$,当然咱们就积分(下图当中的暗影局部)齐全能够依据公式进行求解(如果你不懂积分也没有关系,下文咱们会把这个函数写进去,不会影响你对并发的了解):

$$
\int_0^{10} x^2\mathrm{d}x = \frac{1}{3}x^3+C
$$

然而咱们用程序去求解的时候并不是采纳下面的办法,而是应用微元法:

$$
\int_0^{10} x^2\mathrm{d}x =\sum_{i= 0}^{1000000}(i * 0.00001) ^2 * 0.00001
$$

上面咱们用一个单线程先写出求 $x^2$ 积分的代码:

public class X2 {public static double x2integral(double a, double b) {
    double delta = 0.001;
    return x2integral(a, b, delta);
  }

  /**
   * 这个函数是计算 x^2 a 到 b 地位的积分
   * @param a 计算积分的起始地位
   * @param b 计算积分的最终地位
   * @param delta 示意微元法的微元距离
   * @return x^2 a 到 b 地位的积分后果
   */
  public static double x2integral(double a, double b, double delta) {
    double sum = 0;
    while (a <= b) {sum += delta * Math.pow(a, 2);
      a += delta;
    }
    return sum;
  }

  public static void main(String[] args) {
    // 这个输入的后果为 0.3333333832358528
    // 这个函数计算的是 x^2 0 到 1 之间的积分
    System.out.println(x2integral(0, 1, 0.0000001));
  }
}

下面代码当中的函数 x2integral 次要是用于计算区间 $[a, b]$ 之间的二次函数 $x^2$ 的积分后果,咱们当初来看一下如果咱们想计算区间 [0, 10000] 之间的积分后果且 delta = 0.000001 须要多长时间,其中 delta 示意每一个微元之间的间隔。

public static void main(String[] args) {long start = System.currentTimeMillis();
    System.out.println(x2integral(0, 10000, 0.000001));
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

从下面的后果来看计算区间 [0, 10000] 之间的积分后果且 delta = 0.000001,当初假如咱们应用8 个线程来做这件事,咱们改如何去布局呢?

因为咱们是采纳 8 个线程来做这件事儿,因而咱们能够将这个区间分成 8 段,每个线程去执行一小段,最终咱们将每一个小段的后果加起来就行,整个过程大抵如下。

首先咱们先定义一个继承 Thread 的类(因为咱们要进行多线程计算,所以要继承这个类)去计算区间 [a, b] 之间的函数 $x^2$ 的积分:

class ThreadX2 extends Thread {
    private double a;
    private double b;
    private double sum = 0;
    private double delta = 0.000001;

    public double getSum() {return sum;}

    public void setSum(double sum) {this.sum = sum;}

    public double getDelta() {return delta;}

    public void setDelta(double delta) {this.delta = delta;}

    /**
   * 重写函数 run
   * 计算区间 [a, b] 之间二次函数的积分
   */
    @Override
    public void run() {while (a <= b) {sum += delta * Math.pow(a, 2);
            a += delta;
        }
    }

    public double getA() {return a;}

    public void setA(double a) {this.a = a;}

    public double getB() {return b;}

    public void setB(double b) {this.b = b;}
}

咱们最终开启 8 个线程的代码如下所示:

public static void main(String[] args) throws InterruptedException {
    // 单线程测试计算工夫
    System.out.println("单线程");
    long start = System.currentTimeMillis();
    ThreadX2 x2 = new ThreadX2();
    x2.setA(0);
    x2.setB(1250 * 8);
    x2.start();
    x2.join();
    System.out.println(x2.getSum());
    long end = System.currentTimeMillis();
    System.out.println("破费工夫为:" + (end - start));
    System.out.println("多线程");
    
    // 多线程测试计算工夫
    start = System.currentTimeMillis();
    ThreadX2[] threads = new ThreadX2[8];
    for (int i = 0; i < 8; i++) {threads[i] = new ThreadX2();
        threads[i].setA(i * 1250);
        threads[i].setB((i + 1) * 1250);
    }
    // 这里要期待每一个线程执行实现
    // 因为只有执行实现能力失去计算的后果
    for (ThreadX2 thread : threads) {thread.start();
    }
    for (ThreadX2 thread : threads) {thread.join();
    }
    end = System.currentTimeMillis();
    System.out.println("破费工夫为:" + (end - start));
    double ans = 0;
    for (ThreadX2 thread : threads) {ans += thread.getSum();
    }
    System.out.println(ans);
}
// 输入后果
单线程
3.333332302493948E11
破费工夫为:14527
多线程
破费工夫为:2734
3.333332303236695E11
单线程 多线程(8 个线程)
计算结果 3.333332302493948E11 3.333332303236695E11
执行工夫 14527 2734

从下面的后果来看,当咱们应用多个线程执行的时候破费的工夫比单线程少的多,简直缩小了 7 倍,由此可见并发的“威力”。

FutureTask 机制

在前文和代码当中,咱们发现不论是咱们继承自 Thread 类或者写匿名外部内咱们都没有返回值,咱们的返回值都是 void,那么如果咱们想要咱们的run 函数有返回值怎么办呢?JDK为咱们提供了一个机制,能够让线程执行咱们指定函数并且带有返回值,咱们来看上面的代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FT {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(task);
        thread.start();
        // get 函数如果后果没有计算出来
        // 主线程会在这里阻塞,如果计算
        // 进去了将返回后果
        Integer integer = task.get();
        System.out.println(integer);
    }
}

class  MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {System.out.println("线程正在执行");
        return 101;
    }
}
// 输入后果
线程正在执行
101

从下面的继承构造咱们能够看出 FutureTask 实现了 Runnable 接口,而下面的代码当中咱们最终会将一个 FutureTask 作为参数传入到 Thread 类当中,因而线程最终会执行 FutureTask 当中的 run 办法,而咱们也给 FutureTask 传入了一个 Callable 接口实现类对象,那么咱们就能够在 FutureTask 当中的 run 办法执行咱们传给 FutureTaskCallable接口中实现的 call 办法,而后将 call 办法的返回值保留下来,当咱们应用 FutureTaskget函数去取后果的时候就将 call 函数的返回后果返回回来,在理解这个过程之后你应该能够了解下面代码当中 FutureTask 的应用形式了。

须要留神的一点是,如果咱们在调用 get 函数的时候 call 函数还没有执行实现,get函数会阻塞调用 get 函数的线程,对于这外面的实现还是比较复杂,咱们在之后的文章当中会持续探讨,大家当初只须要在逻辑上了解下面应用 FutureTask 的应用过程就行。

总结

在本篇文章当中次要给大家介绍了一些并发的需要和根底概念,并且应用了一个求积分的例子带大家切身体会并发带来的成果晋升,并且给大家介绍了在 Java 当中 3 中实现并发的形式,并且给大家梳理了一下 FutureTask 的办法的大抵工作过程,帮忙大家更好的了解 FutureTask 的应用形式。除此之外给大家介绍了 join 函数,大家须要好好去了解这一点,认真去理解 join 函数到底是阻塞哪个线程,这个是很容搞错的中央。

以上就是本文所有的内容了,心愿大家有所播种,我是 LeHung,咱们下期再见!!!(记得 点赞 珍藏哦!)


更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0