引言
最近还是一直在准备面试这块的内容,大多数互联网公司面试,避免不了的会问到并发这块的内容,所以,准备写几篇Blog来总结一下Java并发方面知识,于是就先写一篇最基础的,也就是并发编程中涉及到的基础概念,因为主要是想到什么写什么,所以可能会有遗漏,就当看个热闹吧。
这边文章参考了网上众多文章,并加上了自己的理解,也可能会有理解不周到的地方,如有错误,请及时指出。
进程和线程
定义
进程(Process):具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配的一个独立单位(PS:若不支持线程机制,进程也是系统调度的单位。否则,线程是系统调度的单位。因为随着cpu的性能越来越好,所以才将资源分配和调度分开,于是才有了线程这个概念)。比如我们打开一个程序,这个程序就是一个进程。
线程(Thread or Lightweight Process LWP):从其英文定义可以看出,线程也可以被称之为轻量级进程。线程是进程的一个实体,是程序执行流的最小单元。
通常将进程看作是分配资源的最小单位,而线程则是操作系统调度的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程和线程的关系以及区别
1.线程是进程的组成部分,一个进程可以拥有很多线程,但至少有一个线程,但是一个线程只能属于一个进程;
2.操作系统的资源是分配给进程的,同一进程内的线程共享该进程的所有资源,但CPU是分配给线程的,即真正在处理及上运行的是线程;
3、不同的进程使用不同的内存空间,而一个进程内的所有线程共享父进程所拥有的全部空间,这极大提高了程序的运行效率(PS:内存空间不同于栈空间,每个线程都拥有单独的栈内存用来存储本地数据。线程拥有自己的堆栈、自己的程序计数器和自己的局部变量,但他不拥有系统资源);
4.线程的调度和管理由进程本身负责完成。操作系统对进程进行调度,管理和资源分配;
5.同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现;
6.一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程;
7.线程的启动速度快,进程的启动速度慢。
协程
Java原生并不提供对协程的支持,但有的框架模拟出了协程的功能,比如Kilim框架,这里我们就简单提一下协程的概念。
协程(Coroutines),是一种比线程更加轻量级的存在。一个进程可以拥有多个线程,一个线程也可以拥有多个协程。
协程的切换不是被操作系统的内核管理,而是直接由程序控制
线程进程都是同步机制,而协程则是异步。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
协程的暂停完全由程序控制,而线程之间状态的切换是由操作系统的内核实现的,所以协程的开销远远小于线程的开销。
多进程和多线程
就拿我们平时使用电脑来举个例子:
1.我们打开chrome浏览器,用浏览器一边下载文件、一边听歌、一边看网页,这就是多线程;
2.我们同时打开了QQ、微信、浏览器等,这就是多进程。
多进程和多线程的比较
从上面例子,我们从来分析一下多进程和多线程之间的优劣势
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程占优 |
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会相互影响 | 因为共享进程数据,所以导致一旦一个线程挂掉,整个进程也会随之挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
多线程的好处
1.同一进程的线程之间共享内存,但是进程之间不能共享内存;
2.系统创建进程需要重新为其分配系统资源,但是线程并不需要,所以代价会小很多,效率也会更高;
3.资源利用率更好;
4.程序设计更加简单;
5.程序响应更快。
这里可以参考并发编程网
总而言之,随着CPU性能的越来越好,多线程可以让我们充分利用CPU的资源,帮助我们编写出高性能高效率的程序。
并发和并行
介绍
从时间上来说,并行是指两个或多个时间在同一时刻同时发生,而并发则是指两个或多个事件在同一时间段内同时发生;
从空间上来说,并行实在多个不同实体上进行的,而并发是在同一个实体上进行的。
并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
图片解释
并发和并行.png现实场景
举个惊悚点的例子:
普通人只有1个脑子,一边吃饭一边说话,其实有脑子有任务切换在里面,所以是并发。(PS:只是因为这个切换速度非常快,所以让人产生了是并行的感觉)
一个双脑人,一个脑吃饭,一个脑说话,俩脑子彼此独立互不冲突,这是并行。
一个双脑人,一个脑子吃饭+看书,一个脑子说话,这是并行中含有并发(有个脑子在轮换吃饭 和 看书)。
一个三脑人,一个脑子吃饭,一个脑子看书,一个脑子说话,这是并行。
让一个CPU执行多个【可并行 ( 可分解,可单独核心运行)】 的任务,就是并发(concurrence)
多个CPU分别执行【可并行】任务,就是并行(parallel)
在现实中
单核的机器,都是并发 concurrence 执行的。
多核的机器,都是并行 parallel 中嵌套着并发 concurrence 运行的。
如果存在无限核心的机器,则所有任务都可以是并行运行的。
临界区
介绍
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
画图解释
临界区.png现实场景
打印机,我们工作的时候肯定会经常用到打印机,打印机就是一个临界区的最好例子。一台打印机一次只能执行一个任务,如果 A 和 B 同时需要打印文件,很显然,如果 A 先发下打印的任务,打印机就开始打印 A 的文件。B 的任务就只能等待 A 打印结束之后才能打印。
同步和异步、阻塞和非阻塞
同步和异步
介绍
同步和异步通常用来形容一次方法的调用。
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,但调用者并不一定会得到结果,而且调用者可以继续后续的操作。
图片解释
阻塞和非阻塞
这里把同步异步和阻塞非阻塞放在一起,主要是很多时候对这几个概念有点模糊,这里具体说一下。
同步和异步关心的是消息传输机制,而阻塞非阻塞关心的是程序在等待调用结果返回时的状态。
所以上面四种概念,会有四种组合方式,分别是:
同步阻塞、同步非阻塞、异步阻塞和异步非阻塞。
现实场景
这样讲会可能比较抽象,那我们继续结合实际来说一下,首先是烧水的例子,我们拥有两个水壶,一个是会响的水壶,简称响水壶,一个是普通水壶。
1.用普通水壶煮水,并且站在那里,不管水开没开,每隔一定时间看看水开了没。-同步阻塞
2.还是用普通水壶煮水,不再傻傻的站在那里看水开,跑去寝室上网,但是还是会每隔一段时间过来看看水开了没有,水没有开就走人。-同步非阻塞
3.使用高大上的响水壶来煮水,站在那里,但是不会再每隔一段时间去看水开,而是等水开了,水壶会自动的通知他。-异步阻塞
4.还是使用响水壶煮水,跑到客厅上网去,等着响水壶自己把水煮熟了以后通知他。-异步非阻塞
这里可以看出
同步就是烧开水,需要自己去轮询(每隔一段时间去看看水开了没),异步就是水开了,然后水壶会通知你水已经开了,你可以回来处理这些开水了。
同步和异步是相对于操作结果来说,就是会不会等待结果返回。
阻塞就是说在煮水的过程中,你不可以去干其他的事情,非阻塞就是在同样的情况下,可以同时去干其他的事情。阻塞和非阻塞是相对于线程是否被阻塞。
再讲一个下载的例子:
1.同步阻塞:一直盯着下载进度条,到 100% 的时候就完成。
同步体现在:等待下载完成通知;
阻塞体现在:等待下载完成通知过程中,不能做其他任务处理;
2.同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
同步体现在:等待下载完成通知;
非阻塞体现在:等待下载完成通知过程中,去干别的任务了,只是时不时会瞄一眼进度条;【小明必须要在两个任务间切换,关注下载进度】
3.异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音。
异步体现在:下载完成“叮”一声通知;
阻塞体现在:等待下载完成“叮”一声通知过程中,不能做其他任务处理;
4.异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。
异步体现在:下载完成“叮”一声通知;
非阻塞体现在:等待下载完成“叮”一声通知过程中,去干别的任务了,只需要接收“叮”声通知即可;【软件处理下载任务,小明处理其他任务,不需关注进度,只需接收软件“叮”声通知,即可】
同步和异步关注的是消息如何通知的机制,而阻塞和非阻塞关注的则是等待消息通知时的状态,这两个概念是理解同步、异步、阻塞、非阻塞的关键所在。
死锁、饥饿、活锁
死锁
介绍
死锁,是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
例如,线程A已经获取资源R1,线程B已经获取资源R2,之后线程A尝试获取资源R2,这个时候因为资源R2已经被线程B获得了,所以线程A只能阻塞直到线程B释放资源R2。另一方面,线程B在已经获得资源R2的前提下尝试获取由线程A持有的资源R1,那么由于资源R1已经被线程A持有了,那么线程B只能被阻塞直到线程A释放资源R1。这样线程A和线程B都在等待对方持有的资源,就造成了死锁。
死锁的四个条件
1.互斥条件:线程使用的资源必须至少有一个是不能共享的(至少有锁);
2.请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源(至少两个线程持有不同锁,又在等待对方持有锁);
3.非剥夺条件:分配资源不能从相应的线程中被强制剥夺(不能强行获取被其他线程持有锁);
4.循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程(线程A等线程B;线程B等线程C;...;线程N等线程A。如此形成环路)。
避免死锁的方法
1.尽可能减少锁的范围,如在Java中尽量使用同步代码块而不使用同步方法;
2.尽量不编写在同一时刻获取多个锁的代码,因为在一个线程持有多个资源的时候很容易发生死锁;
3.根据情况将过大范围的锁进行切分,让每个锁的作用范围减小,从而降低死锁发生的概率。这以原则的典型应用是ConcurrentHashMap的锁分段技术。
饥饿
饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
产生这种情况的原因是多种的,可能是它的线程优先级太低,而高优先级的线程不抢占它需要的资源,导致低优先级线程无法工作。也可能是某个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行。
活锁
活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。
举个现实生活中的例子,一条狭窄的道路,男子A和女子B迎面相遇了,缘分使然,A绅士的想让出道路让B先过,这时B也保持良好的淑女形象想让出道路让A先过,导致两人都避让了;两人尴尬一笑,之后A想着B既然让了,就准备先过,巧的是,这时B心里也想着,A既然让了,不如先过,两人又撞上了;然后又开始礼貌性的相互避让,避让之后各自又想先走结果又撞上了,结果两人都没过去。这种情况就是活锁。
线程都秉承着"谦让"的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程之间跳动,而没有一个线程可以同时拿到所有资源而正常执行。
活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。
参考
本文参考众多文章总计而成,这里参考太多,所以只列举比较重要的几篇文章。
Java多线程和并发性知识点总结
并发编程的几个概念
聊聊同步、异步、阻塞与非阻塞
网友评论