Java多线程基础知识---并发与并行基础-1

时间:2023-01-21 17:31:05

一、并发与并行基础

0.抓住那个CPU

  1. 计算机的运算速度非常快,但是在我写上一句话的时候,我想表达的最深层的意思是:CPU的运行速度很快。早在第一台计算机(对,就是那个贼大的,一个房间那么大的计算机)问世的时候,CPU执行运算的速度就直接将我们(也就是在压榨CPU计算力的外部人员,还有IO设备)远远的抛到后面,现代CPU的计算能力早就不是早期CPU所能媲美的,速度比起人类来,快的那叫一个丧心病狂。现在你可以想象以下这个场景:

    你在用计算机计算你的一道线性代数题目,你在键盘上输入你的表达式、限制条件,大概单身20年的手速也要那么几秒钟,如果你电脑的CPU只干一件事(当然是不可能的):等你输入表达式完成后计算其结果。然后你惊讶的发现:你冥思苦想算不出来的几元几次方程,刚敲确定键电脑就给算出来了,当你惊讶于CPU的高效率时,是否想过,在等你输入的这段时间里,以CPU那么优秀的能力,在这段时间CPU是不是很闲?

  2. 在刚才的场景中,CPU的确很闲。但是在实际的使用场景中,你可能一边输入着表达式,一边听着网易云的日推荐,还在后台下着百度云资源,再加一个什么QQ、微信什么的,但是你发现在你按确定键的时候,它还是马上把它算出来了!并且你的音乐也没有停,百度云也还在下载着资源,但是比起上一个场景,就好像CPU有分身术一样,同时的处理着多个任务,而且这多个任务好像是在同时运行的,并没有间断一样,至少你没有发现。打开计算机的任务管理器会看见如下的情景:
    Java多线程基础知识---并发与并行基础-1
    看见了吧?CPU其实很同时在干很多事情的!

  3. 刚才的场景,我并没有感觉到在我写博文的时候网易云的音乐停止,所以在我感觉来看,这些任务是同时在进行的(当然还有另外的极端情况:打开软件太多了,会感觉明显的卡顿:任务太多了,CPU都受不了了,同时CPU的占有率会狂飙)。
    刚才所瞎扯的,是进程层面上的并发。并发的最根本目的就是为了更好的压榨CPU这个超级劳动力,为我们做更多的事情,让优秀的CPU同学多发光发热。

1.硬件工程师的甩锅

想必你一定听说过摩尔定律,直白的说就是:每18-24个月,我们的CPU处理能力将翻倍。但是万事都是有极限的,CPU主频在接近4GHz的时候就已经很难上升了(因为芯片组件直径已经达到纳米级别),然后硬件工程师突发奇想的把几个CPU内核集成,于是就有了多核处理器(主要是服务器的硬件,我的微机是单核的)。现在计算机的内核数量不断地在上升,在提高计算机计算性能的时候也在考验着软件工程师:如何发挥出多核计算机系统的最大计算潜力,答案就是:并行。
然后就出来了这个乱七八糟、苦不堪言的多线程问题。你一定会认同我的这个观点,如果你学多线程的话。

2.并发和并行的区别

你应该听说过一个和并发很像的词语:并行。但是并发和并行两个之间有什么联系和又有区别呢?

并发:简单的并发理解就是多个任务在高速的切换运行,让人仿佛以为他们在同时运行。
其实不是的,并发的概念主要是针对单核处理机(只有一个物理内核的处理机或者多核处理机的一个具体的物理内核),因为只有一个CPU,所以多个任务只能间接的占用CPU执行,每个任务会占用CPU的一定时间(CPU的进程调度,视具体的调度算法而定,简单的就是时间片轮转调度算法),但是这个时间间隔人类几乎无法察觉(可能是10-100ms,或者更短,也许你会觉得太短了,但是这点时间已经可以让CPU执行上千条指令),所以就算是任务是间隔运行的,我们会产生我们的任务在同时运行的错觉(当然,任务太多了,CPU也忙不过来,所以你就可以察觉了,比如你的软件变得很卡)。
并行:简单的并行概念也是同时运行多个任务,是真正意义的同时运行。并行的概念是针对多核处理机的,(这里和上面所提到的内核都是物理内核,而不是超线程技术实现的逻辑核,逻辑核是和并发相识的概念),多核处理机的多个CPU内核可以做到真正的同时进行,有多少个物理内核,就可以有多少个并行的任务,互不干涉。

注:进程的上下文切换和多CPU分别运行一个进程的一部分线程的情况就涉及到操作系统太深了,我就不提了(主要是不太了解),同时在下面的博文中,将不再对并发和并行进行区分,统一称为:并发。

3.并发的思想基础

并发编程还需要思想基础?没错!并发编程必须要有并发的编程思想基础(也就是做好头疼的准备,并发会挑战认知极限),并发编程与顺序编程的最大不同就是:
我不能控制其执行的行为,包括他什么时候开始,什么时候得到结果。在并发编程中,切记:一切非原子性操作(最基本的计算机指令,要么都成功,要么都失败)都可能在任何步骤被打断。

举一个荔枝(interesting):世界被上帝诅咒了(上帝不会诅咒自己),每一次只能有一个人行动,但是让哪一个人行动是上帝的权利。在这样的世界里,张三、李四快乐的生活着,一天他们在吃饭,菜在桌子上的。吃到菜需要伸手、夹菜、放到嘴里三个步骤。他们感情很好,不会抢别人筷子上的东西。那么吃到菜的三个动作就是原子性操作,不可打断。但是注意一点,吃菜需要三步。好!现在他们开始吃饭了。上帝先让张三可以行动,于是张三伸出了手准备夹那块最大的红烧肉,这个时候不知道为什么上帝生气了(就像不知道女朋友为什么生气一样,可能觉得他太贪心了),上帝剥夺了张三可以活动的权利(手一直伸着),然后让李四可以行动。李四先伸出手,再夹了一块青菜,再放到嘴里,再伸出手,夹了那块最大的火烧肉,放到了嘴里。这个时候上帝不生气了(可能觉得李四也不咋地),重新让张三可以行动(自然李四就不能行动了),这个时候张三就会一脸蒙逼:我要夹的那块最大的红烧肉呢?

说上面的例子是为了说明一个问题,在计算机的世界,任何进程的非原子性操作都可能被马上停止(也就是中断当前任务去忙其他的任务),也就是CPU随时都可能去忙别的任务,这一点思想在并发编程里面至关重要。

4.共享资源

共享资源,也叫临界资源。指的是那种会在多个任务间共享的一些信息、设备(也就是计算机系统里面的资源),比如一些公共的变量,共享的系统设备:如打印机等,对共享资源的访问和使用必须要小心翼翼,不然就会出现上一个红烧肉例子的那种情况,需要的东西拿不到(或者不是你实现想象的该资源应该有的样子,因为他的状态被别人修改了)。再打个比方,如果不对打印机的使用进行控制,同时打印两份文档打印出来的东西肯定不是你想要的,所以,对这些危险的东西的访问必须遵循一些原则,也就是要守规矩。
在多线程中,最麻烦的一个部分就是对临界资源的安全访问。

5. 同步和异步,阻塞与非阻塞的概念

同步和异步都是用来形容一次方法的调用。

同步就是:我让你去给我买只雪糕,在你没有把雪糕买回来之前,我不会做任何事情,等你把雪糕买回来了,我就吃雪糕。
异步就是:我让你去给我买只雪糕,给你说了之后我就继续码代码了,等你把雪糕买回来了,我就吃雪糕。

阻塞与非阻塞是形容任务,也就是线程之间的影响程度。

例如:墙上一个表,大家都可以同时看表上的时间,那么我们看表的这个动作就是非阻塞的,因为我看表不会影响你看表。但是比如我们两个只有一只眼镜,在我用这副眼镜的时候你就不能用,那么我用眼镜的这个动作对你用眼镜的动作就是阻塞的。而上面的眼镜和表就是临界资源,临界资源的访问控制要视情况而定(是否会改变资源的状态)。

6.死锁、活锁、饥饿的概念

再举个栗子:我们两个吃饭,一碗饭,一双筷子,现在有以下的场景:

场景一:我拿了一只筷子,你拿了一只筷子,我想要你的那只筷子,但是我们都很饿,你肯定不会给,我也肯定不会给你,那么我们都只有一只筷子,无法吃饭,然后我两就饿死了,所以叫死锁。开个玩笑,实际上是因为我们都没有办法继续我们吃饭的动作,因为我们都还差一点资源,也就是另一双筷子。
场景二:我们很有礼貌,如果我拿不到另外的一只筷子,就会把我的这只筷子放下来,然后再重新去拿。这个时候就可能出现我拿一只,你马上拿了另一只,我放下这一只,你马上放下另一只的死循环。然后大家都吃不到饭,这种情况叫活锁。
场景三:我的速度比较快,我先拿了一只筷子,再哪里另外一只筷子,然后吃饭,放下筷子,再不停的重复,直到吃饱了。你肯定会问,为什么你没有拿到筷子?这个是因为一些其他的原因,比如线程优先级。高优先级可以有更多的CPU执行机会,所以看起来高优先级的线程执行会更快一下。但是你也是可以先拿到筷子(小概率事件在大量事件中必然发生),那么我只有把筷子让给你了哦,但是你吃的少一些,吃完了可能都是饥饿的,所以这种情况叫饥饿。极端的情况会把低优先级的线程任务直接饿死,一直拿不到CPU的执行权。

7.小结

在以上的内容中,简单提了一下什么是多任务以及多任务会出现的问题,当然作重要的还是上帝的那个例子,多线程的基本思想,有了这些可以开启Java多线程之旅了。

[注]:详细知识请参见:计算机操作系统。