本系列将对Java多线程进行简单的介绍。
分为上中下三个章节。
上篇对操作系统中关于进程、并发的相关概念以及问题进行了介绍;
中篇对Java多线程的基础进行介绍;
下篇将会对Java多线程编程提供的工具、模式进行介绍;
Java多线程,首先需要了解线程,了解线程又需要对进程有所了解,而了解进程你需要知道程序的概念,知道程序的概念,你还需要了解操作系统。
线程与操作系统
操作系统是对计算机硬件资源的管理程序,是应用程序与计算机硬件交互的中间层,其本质仍旧是运行于硬件电路上的程序
对计算机硬件来说不存在操作系统,只是处理器对指令的执行,不过操作系统是一个特殊一点的程序。
而对于应用开发者来说,以JavaWeb为例,我们却接触了太多的东西,首先是Java语言本身,然后...........
servlet?jsp?MVC?Spring?SpringBoot?ORM?Mybatis?Dubbo?
然而,这些其实仍旧还是Java本身--Java语言编写的程序,纵然有那么多的规范,协议,他也只是一个Java编写的程序
所以不管你用了多少技术,框架,模式,实现了怎么样的协议与功能,原理是什么,也只是人类意识层面上的内容,到底层只有指令。
用到的一些应用软件,MYSQL?REDIS?也只是程序。
所以,运行于计算机之上的这一切都只是程序
这些程序经过指定的步骤,从高级到低级,从人类可以理解到无法识别,最终转换为计算机可以识别的指令。
我们编写的所有的源代码,最终都要转换成计算机系统可以识别的内容,而计算机系统包括硬件以及运行其上的系统软件。
我们所有的编码,都是面向指定的语法,而这门语言本身,则是面向操作系统的,因为外部软件通常是不能直接操纵硬件资源,需要借助于操作系统。
所以某种程度上可以这样认为,所有的源代码都是面向语言的,而语言本身面向操作系统。
操作系统提供了对于计算机硬件资源的管理,对于这些资源的访问,提供了一系列的方法途径,这些途径方法如同机器的操作面板,如同驾驶舱的按钮手柄。
所以说,计算机有什么不重要,计算机操作系统有什么才重要。最简单的例子就是重装系统后,如果没有网卡驱动,你的电脑将无法了解Internet,尽管你的网卡就好端端的插在哪里。
对绝大多数应用程序员来说,操作系统,便是神一样的存在,所有的一切都要仰仗于他。
什么是程序?
遵循某种语言的源代码经过编译、翻译等步骤转换后的一组计算机能识别和执行的指令,这就是程序。
这是一种静态的资源,当你的电脑中安装一个软件后,如果不启动软件,该软件仅仅是占用磁盘空间
一个程序就像一个用汉字(程序设计语言)写下的红烧肉菜谱(程序),用于指导懂汉语和烹饪手法的人来做这个菜。菜谱就是存在于纸上的文字。
当程序需要运行时,操作系统会加载该程序的信息到内存中,并且分配CPU时间片以及其他硬件硬件资源,并且会对这些资源进行管理,比如数据加载到内存的什么位置了?
而且,现代操作系统都可以同时并发执行多个程序,内存中的这些数据又都是哪个程序的?某个软件在进行切换时执行到哪里了?等等这些都需要操作系统进行管理
操作系统将程序的一次运行抽象为进程
简言之,如果 你(处理器)按照 菜谱(程序)去 做菜(执行程序),这个过程就叫做 下厨做饭(进程)
抽象的概念,没有人会陌生,如果我们想使用Java语言描述一个学生,我们可能会创建一个Student类,里面有各种属性,比如姓名、年龄等
public class Student { private Long id;// id private String name;// 姓名 private Integer age;// 年龄 private String sex;// 性别 //.............等等
这样一个Class就是一个数据结构,通过他对学生进行描述
而进程是操作系统对程序的一次执行的抽象,也就是说一个程序运行需要哪些信息、数据?这些所有的数据项集合就叫做进程。简言之就是一个程序运行所需信息的描述集合。
我们以类来比喻的话可能是这样子:
public class 进程 { private Long 进程号; private String 程序计数器PC; private String xxx寄存器; private String 堆栈内容; // ............................等等 }
还有一个概念是进程上下文,刚才说到现代系统还可以并发的执行多道程序,必然存在着CPU的切换,那么从一个程序切换到另一个程序时,如何才能够恢复?
既然进程是程序的一次运行过程中所需要信息的集合,如果在切换时,将这一瞬时状态,这一集合体各项数据记录下来,当再次切换回来时,只需要将数据恢复不就好了吗
进程执行活动全过程的这一个静态描述叫做进程上下文
进程间的切换,也被称之为上下文切换。
通俗比喻:
如果只有一个厨房,你做菜做一半了,然后需要让出来厨房让别人做,你需要做什么?
收拾好你的食材,记住你刚才食材放置的位置以及处理的进度,哪个菜洗过了?盐放过了么?。。。等等这些数据就是进程上下文,当别人撤出去之后,你需要将这些状态还原,这就是上下文切换。
随着现代计算机技术的发展,进程的弊端开始出现,由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程,线程就是轻量级的进程。
进程仍然是资源分配的基本单位,线程是程序执行的最小单位
线程的出现可以理解为计算机操作系统对于程序的执行进行了更加精细化的控制,将资源分配,程序运行进行了更加细致的分工。
每个线程都运行在进程的上下文中,共享同样的代码和全局数据,很显然,多线程比多进程更容易共享数据。
总之,线程的出现是操作系统技术的发展,为了更加细化分工,节省开销的一种做法,是在进程的基础上发展而来的。
并发与并行
下面这幅图可以很好地解释并发与并行
一个咖啡机两个队伍,就是并发;两个咖啡机,两个队伍,就是并行。
并发 concurrent ,通过CPU调度算法,进行进程间的切换,也就是多任务执行,操作系统将CPU时间片分配给每个进程,给人并行处理的感觉
并行 parallel,并行就是同时执行的意思,多个CPU或者多个机器同时执行一段处理逻辑,是真正的同时。
多线程
很久很久很久以前,操作系统以串行的方式运行,当正在执行的程序遇到阻塞操作,比如等待IO时,CPU空闲等待,极大地浪费了CPU
所以后来出现了多任务操作系统,可以对程序进行切换,当遇到阻塞操作时,CPU可以去执行另外的程序,提高了CPU的利用率
对于线程也是如此,多线程技术相当于是应用程序内部的“多任务”。
就好比一个应用程序内部有多个线程,其中一个线程等待IO操作时,可以切换执行其他的线程,完成其他的任务,所以对于多线程编写的程序,看起来程序能够更快的完成。
所以刚才说线程是操作系统对于程序运行过程的更加细致的划分与掌控,对于一个多线程程序,能够更加充分的利用CPU资源,看起来执行快了,是因为CPU的效率变高了,而不是程序的运行所需时间变少了
对于一个单CPU系统,对于多任务的实现就是并发,操作系统不断地进行着切换,将时间片分配给不同的程序,以看起来像多个程序是共同运行的。
通过多线程,将一个应用程序本身拆解为多任务,如果像上面说的某个线程等待IO导致阻塞,可以执行其他的线程任务,那么将会提高CPU的利用率
但是如果是类似1+2+3+4......+N的计算呢?假设计算过程是均等的,这不会出现IO阻塞的情况,每一次的运算都是相同的,CPU本身也没有空闲等待的浪费,所以CPU利用率没有上升,相反还会有线程切换维护的开销,所以整体看性能或许略有下降。
所以说,单核场景下,尽管多线程在有些场景下可以提高CPU的利用率,但是对于单CPU系统(单核)系统,在有些场景下,反而会降低整体性能。
因为有的时候你并不能提高利用率;而且有的时候即使提高了利用率,如果提高的那一部分利用率,还不足以抵消做的那些不该做的事情的开销,整体看并不一定是往好的方向发展。
很显然,对于单CPU(单核)尽管有些场景多线程可以提高利用率,但是有时也并不能,所以多线程编程并没有强势发展。
但是后来,CPU主频的发展越来越缓慢,对于CPU主频的升级,摩尔定律开始失效了,因为发展太快,集成电路越来越接近极限了。
既然纵向不能发展,人们总是有办法的,开始横向发展,不再追究单核的计算速度,而是研究如何能够将多个独立的计算单元整合到一个CPU中,也就是现在说的多核。
随着技术的发展, 能够装载的核心数目越来越多
对于多核CPU,能够真正的做到在同一瞬时,执行多个线程,是真正的并行。
所以很显然,这种场景对于真正的并行,不管你的程序任务是什么样子的,对于多线程程序,必然能够提高程序的执行速度。
如果只要一个老师辅导三个学生,你需要合理的安排时间任务,才有可能提高整体的效率;但是如果三个学生对应着三个老师同时在辅导,整体的效率肯定是提高的。
所以随着多核CPU以及超线程技术的发展,多线程编程就显得格外重要。
如果单核CPU的性能可以无限制的快速提高,软件开发者完全不用关心多线程编程,一切交给CPU就好了
但是,目前的情况却是CPU的性能已经达到瓶颈,硬件在横向发展,所以如果想要提高CPU的利用率,让你的程序更快的执行,你将不得不面对多线程编程。
《实战Java高并发程序设计》中提到:“顶级计算机科学家唐纳德·尔文·克努斯(Donald Ervin Knuth ),如此评价这种情况:
在我看来,这种现象(并发)或多或少是由于硬件设计者己经无计可施了导致的,他们将摩尔定律失效的责任推脱给软件开发者。”
也说明了这个问题----现在为什么要更加关注多线程技术?
多核场景以及超线程技术的发展下,不是你主动地想要去使用多线程技术,而是现有的硬件体系,想要获得更好地程序性能,你将不得不使用多线程技术进行编程。
当我处理器还是只能一个一个的来的时候,你们是不是多线程并没有那么重要
但是当我可以瞬时同时处理多个线程的时候,如果你还是只有一个线程,你每一时刻也只会有一个线程在执行,但是别人-多线程程序,可能就是多个,所以你的程序的速度与别人相比怎么样?
尽管借助于多线程技术,因为有线程切换等系统开销,所以总共需要CPU做的事情,要大于单线程的时候;
但是CPU多核的并行处理能力以及CPU利用率的提高,将会大大的提高程序的整体效率
所以在多核时代,多线程是必须要考虑的问题。
总结
不管是进程还是线程,都是操作系统对于程序执行的抽象描述,是相关数据:寄存器状态、堆栈值等所有相关数据的集合。
通过进程的相关信息的维护管理,操作系统保障多道程序可以顺利的切换执行;
而对于多线程的应用程序,需要开发者对线程的数据等相关信息进行控制,以保证多线程间可以正确的运行。
多线程共享进程资源,而有些资源是互斥的,并不能允许同时访问,比如对计数器+1,如果临界区代码可以同时访问,可能两个人同时过来,每个人同时从1开始执行加1操作,结果却是2,这显然是不正确的
多线程编程需要解决的核心就是互斥资源的访问以及如何高效的利用CPU。
保障资源的互斥访问是为了保证程序的正确性,否则再快的程序也没有意义;如果编写的程序非常的不合理,逻辑不清晰,反而可能会带来性能问题,而不是提高效率。
所以多线程相关的技术的确很复杂,而且非常容易出错,而且学习成本很高,但是,他终归是为了提高CPU的利用率的同时并且保障临界资源的正确访问。
作为多线程编程人员,如同交警,你需要合理的指挥,提高路口的通行效率,尽最大可能缓解交通堵塞情况,而且需要保证不能在你的指挥下还发生了交通事故或者造成了更大的拥堵;
这是两个主要方面,就是前面提到的效率和互斥访问。
另外路口我应该清场出来多大空间用来调度指挥?(锁粒度)过几分钟这个方向的走,过几分钟那个方向的走(锁时间)?我是轮流几秒钟切换下?还是哪边车多让哪边多走一会还是怎么样(锁偏向)?这些细节非常复杂繁琐。
在未来的一段时间内,多线程编程模型是必然的趋势,也是程序员必须要面对的一件事情,过去的单处理器系统,并发可能是多余的,但是今天,已经成为了势不可挡的趋势。
随着技术的发展,多线程的开发也在从复杂往简单的方向演化(尽管现在仍旧看起来很复杂),随后可能会慢慢地出现很多集成、封装、框架等以让多线程编程更加简单
就如同EJB-Spring-SpringBoot的发展,企业级应用的开发过程一直在简化,但是核心原理却不断的被封装在深处,如果不了解底层,只会招式,永远也打不出来有力的拳头,所以建议大家尽可能的深入学习多线程
本系列文章作为自己的学习记录,从操作系统中关于进程线程并发的相关概念切入,开始介绍Java多线程编程。