• 中文
    • English
  • 注册
  • 查看作者
  • 带你从0到1建立并发知识体系的基石

    在本篇文章当中主要跟大家介绍并发的基础知识,从最基本的问题出发层层深入,帮助大家了解并发知识,并且打好并发的基础,为后面深入学习并发提供保证。本篇文章的篇章结构如下:

    带你从0到1建立并发知识体系的基石

    并发的需求

    • 我们常用的软件就可能会有这种需求,对于一种软件我们可能有多重需求,程序可能一边在运行一边在后台更新,因此在很多情况下对于一个进程或者一个任务来说可能想要同时执行两个不同的子任务,因此就需要在一个进程当中产生多个子线程,不同的线程执行不同的任务。

    • 现在的机器的CPU核心个数一般都有很多个,比如现在一般的电脑都会有4个CPU核心,而每一个CPU核心在同一个时刻都可以执行一个任务,因此为了充分利用CPU的计算资源,我们可以让这多个CPU核心同时执行不同的任务,让他们同时工作起来,而不是空闲没有事可做。

    带你从0到1建立并发知识体系的基石

    • 还有就是在科学计算和高性能计算领域有这样的需求,比如矩阵计算,如果一个线程进行计算的话需要很长的时间,那么我们就可能使用多核的优势,让多个CPU同时进行计算,这样一个计算任务的计算时间就会比之前少很多,比如一个任务单线程的计算时间为24小时,如果我们有24个CPU核心,那么我们的计算任务可能在1-2小时就计算完成了,可以节约非常多的时间。

    并发的基础概念

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

    • 进程简单的说来就是一个程序的执行,比如说你在windows操作系统当中双击一个程序,在linux当中在命令行执行一条命令等等,就会产生一个进程,总之进程是一个独立的主体,他可以被操作系统调度和执行。

    • 而线程必须依赖进程执行,只有在进程当中才能产生线程,现在通常会将线程称为轻量级进程(Light Weight Process)。一个进程可以产生多个线程,二者多个线程之间共享进程当中的某些数据,比如全局数据区的数据,但是线程的本地数据是不进行共享的。

    带你从0到1建立并发知识体系的基石

    Java实现并发

    继承Thread类

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

    带你从0到1建立并发知识体系的基石

    使用匿名内部类实现runnable接口

    当然你也可以采用Lambda函数去实现:

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

    理解主线程和Join函数

    在完成上面的任务之前,首先我们需要明白什么是主线程和子线程,所谓主线程就是在执行Java程序的时候不是通过new Thread操作这样显示的创建的线程。比如在我们的非并发的程序当中,执行程序的线程就是主线程。

    比如在上面的代码当中执行语句System.out.println(“我是主线程”);的线程就是主线程。

    现在我们再来看一下我们之前的任务:

    上面的任务很明确就是主线程在执行输出自己线程的名字的语句之前,必须等待子线程执行完成,而在Java线程当中给我提供了一种方式,帮助我们实现这一点,可以保证主线程的某段代码可以在子线程执行完成之后再执行。

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

    带你从0到1建立并发知识体系的基石

    我们需要知道的一点是thread.join()这条语句是主线程执行的,它的主要功能就是等待线程thread执行完成,只有thread执行完成之后主线程才会继续执行thread.join()后面的语句。

    第一个并发任务——求x2的积分

    接下来我们用一个例子去具体体会并发带来的效果提升。我们的这个例子就是求函数的积分,我们的函数为最简单的二次函数x2,当然我们就积分(下图当中的阴影部分)完全可以根据公式进行求解(如果你不懂积分也没有关系,下文我们会把这个函数写出来,不会影响你对并发的理解):

    ∫010x2dx=13×3+C

    带你从0到1建立并发知识体系的基石

    但是我们用程序去求解的时候并不是采用上面的方法,而是使用微元法:

    ∫010x2dx=∑i=01000000(i∗0.00001)2∗0.00001

    带你从0到1建立并发知识体系的基石

    下面我们用一个单线程先写出求x2积分的代码:

    上面代码当中的函数x2integral主要是用于计算区间[a,b]之间的二次函数x2的积分结果,我们现在来看一下如果我们想计算区间[0, 10000]之间的积分结果且delta = 0.000001需要多长时间,其中delta表示每一个微元之间的距离。

    从上面的结果来看计算区间[0, 10000]之间的积分结果且delta = 0.000001,现在假设我们使用8个线程来做这件事,我们该如何去规划呢?

    因为我们是采用8个线程来做这件事儿,因此我们可以将这个区间分成8段,每个线程去执行一小段,最终我们将每一个小段的结果加起来就行,整个过程大致如下。

    带你从0到1建立并发知识体系的基石

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

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

    从上面的结果来看,当我们使用多个线程执行的时候花费的时间比单线程少的多,几乎减少了7倍,由此可见并发的“威力”。

    FutureTask机制

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

    带你从0到1建立并发知识体系的基石

    从上面的继承结构我们可以看出FutureTask实现了Runnable接口,而上面的代码当中我们最终会将一个FutureTask作为参数传入到Thread类当中,因此线程最终会执行FutureTask当中的run方法,而我们也给FutureTask传入了一个Callable接口实现类对象,那么我们就可以在FutureTask当中的run方法执行我们传给FutureTask的Callable接口中实现的call方法,然后将call方法的返回值保存下来,当我们使用FutureTask的get函数去取结果的时候就将call函数的返回结果返回回来,在了解这个过程之后你应该可以理解上面代码当中FutureTask的使用方式了。

    需要注意的一点是,如果我们在调用get函数的时候call函数还没有执行完成,get函数会阻塞调用get函数的线程,关于这里面的实现还是比较复杂,我们在之后的文章当中会继续讨论,大家现在只需要在逻辑上理解上面使用FutureTask的使用过程就行。

    总结

    在本篇文章当中主要给大家介绍了一些并发的需求和基础概念,并且使用了一个求积分的例子带大家切身体会并发带来的效果提升,并且给大家介绍了在Java当中3中实现并发的方式,并且给大家梳理了一下FutureTask的方法的大致工作过程,帮助大家更好的理解FutureTask的使用方式。除此之外给大家介绍了join函数,大家需要好好去理解这一点,仔细去了解join函数到底是阻塞哪个线程,这个是很容搞错的地方。

    以上就是本文所有的内容了,希望大家有所收获,我们下期再见!!!(记得 点赞 收藏哦!)

  • 0
  • 0
  • 0
  • 19
  • 请登录之后再进行评论

    登录
  • 任务
  • 实时动态
  • 发布
  • 单栏布局 侧栏位置: