一、简介
1、并发简史
阶段一:早期的计算机中不包含操作系统,它们从头到尾只能执行一个程序,并且这个程序能够访问计算机的所有资源,在这种环境下程序不仅很难编写和运行,而且对计算机资源也是一种浪费。
阶段二:操作系统以及进程的出现,使得计算机能够同时运行多个程序,每个程序在单独的进程中运行;操作系统为每个独立的进程分配资源,包括内存、文件句柄以及安全证书;同时进程间的相互通信可以通过一些粗粒度的通信机制来交换数据,比如:套接字、信号输出器、共享内存、信号量以及文件。
进程是资源分配的基本单位,具有以下优点:
- 资源利用率
- 公平性 - 不同的用户与程序对计算机上的资源具有同等的使用权
- 便利性
阶段三:线程的出现 促使进程的出现的因素 (资源利用率、公平性、遍历性) 同样也促使了线程的出现;线程会共享进程范围内的共享资源,例如内存句柄和文件句柄,每个线程拥有各自的程序计数器 (Program Counter)、栈以及局部变量,同时多个线程可以调度在多个CPU处理器上运行。
线程也称之为轻量级进程,是资源调度与执行的基本单位;由于同一个进程中的多个线程共享进程的内存空间,因此这些线程都能够访问相同的变量并在同一个堆上分配对象。---- 这就需要一种比进程间共享数据更细粒度的数据共享机制,保证数据的同步。
2、线程的优势
线程能够提高复杂系统的性能,有效降低程序的开发和维护成本,降低代码的复杂度、使得代码更容易编写、阅读与维护。
2.1 发挥多处理器的强大功能
多个线程可以在多个处理器上执行,提高处理器资源的利用率来提升系统吞吐量;同时使用多线程也有助于提升单个处理器系统的吞吐率
2.2 建模型的简单性
对于每个线程来说只执行某一种特定的任务,无需管理不同任务之间的优先级与执行时间,以及任务的切换。
2.3 异步事件的简化处理
服务器应用程序在接受来自多个远程客户端的套接字连接请求时,如果为每个连接都分配其各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。(在单线程应用程序中,一旦一个线程被阻塞,其他线程都将停顿,这是就必须使用非阻塞I/O,但该I/O的复杂程度远远高于同步I/O,并且很容易出错)
3、线程所带来的挑战
Java对线程的支持其实是一把双刃剑。虽然Java提供了相应的语言和库,以及一种明确的跨平台内存模型(该内存模型实现了在Java中开发“编写一次,随处运行”的并发应用程序),这些工具简化了并发应用程序的开发,但同时也提高了对开发人员的技术要求。
3.1 安全性问题
多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量。程会由于无法预料的数据变化而发生错误。
3.2 死锁问题
由于多线程情况下存在资源的竞争,此时就会产生一种情况,线程A持有了线程B的资源1,线程B持有了线程A的资源2,双方想要对方资源却又不释放对方资源,在这种情况下就会一直等待下去。
3.3 性能问题
频繁的上下文切换:
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作(Context Switch),这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。
线程的同步机制:
对于线程的共享数据,需要使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。
二、基本概念
1、进程、线程、管程
进程:
程序的一次执行过程或一个正在运行的程序,它是资源分配的单位,每个进程拥有自己的一套变量。
线程:
程序内部的一条执行路径,它是调度和执行的单位,每个线程拥有自己的局部变量表、程序计数器(指向正在执行的指令指针)以及各自的生命周期。
现代操作系统中一般不止一个线程在运行,当启动了一个Java虚拟机(JVM)时,从操作系统开始就会创建一个新的进程(JVM进程),JVM进程中将会派生或者创建很多线程。
一个进程的多个线程共享同一个内存单元,从同一个堆中分配对象,可以访问相同的变量和对象,使得线程之间的通信变得简便、高效,但同时会引发安全隐患。
管程:
即同步监视器(Monitor),在Java中每个对象都有与之相关联的同步监视器,JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的。Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
Object o = new Object();
new Thread(() -> {
synchronized (o)
{
}
},"t1").start();
2、并发、并行
并行:
多个cpu在同一时间点执行多个线程。例如:多个人做不同的事。同一个时间点做多件事。
并发:
一个cpu在同一个时间段内执行多个线程。例如:秒杀,多个人做同一件事。同一个时间段做多件事。
3、同步与异步、消息通知
同步与异步:
同步与异步描述的是被调用者的(即下文的B)。
如A调用B:
如果是同步,B在接到A的调用后,会立即执行要做的事。A的本次调用可以得到结果。
如果是异步,B在接到A的调用后,不保证会立即执行要做的事,但是保证会去做,B在做好了之后会通知A。A的本次调用得不到结果,但是B执行完之后会通知A。
消息通知:
异步的概念和同步相对。当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知 和 回调来通知调用者。
这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。使用哪一种通知机制,依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制:
- 如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。
- 如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
4、阻塞与非阻塞
阻塞与非阻塞描述的是调用者的(下文的A)
如A调用B:
如果是阻塞,A在发出调用后,要一直等待,等着B返回结果。
如果是非阻塞,A在发出调用后,不需要等待,可以去做自己的事情。
同步不一定阻塞,异步也不一定非阻塞。没有必然关系。
老张烧水:
1 老张把水壶放到火上,一直在水壶旁等着水开。(同步阻塞)
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
3 老张把响水壶放到火上,一直在水壶旁等着水开。(异步阻塞)
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
1和2的区别是,调用方在得到返回之前所做的事情不一行。 1和3的区别是,被调用方对于烧水的处理不一样(存在 “响” 的通知,即上文提到的消息通知)。
参考:
Java并发编程实战