Java并发的背景
并发与操作系统的生命历程息息相关。进程的出现,使得程序状态的保存变为现实,为进程间的切换提供了可能,实现了操作系统的并发,大大提高资源利用率。虽然进程的出现解决了操作系统的并发问题,但人们对实时性又有了更高的要求。由于一个进程由若干个子任务组成,所以人们就发明了线程,让每个线程负责一个独立的子任务,提高程序的响应灵敏度。一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。因此,虽然多线程提高了资源利用率,保证了实时性,但同时也带来了包括安全性、活跃性和性能等问题。总的来说,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。
一. 进程和线程的由来
(1). 操作系统中为什么会出现进程?
说起进程的由来,我们需要从操作系统的发展历史谈起。
也许在今天,我们无法想象在很多年以前计算机是什么样子。我们现在可以用计算机来做很多事情:办公、娱乐、上网,但是在 计算机刚出现的时候,是为了解决数学计算的问题,因为很多大量的计算通过人力去完成是很耗时间和人力成本的。 在最初的时候,计算机只能接受一些特定的指令,用户输入一个指令,计算机就做一个操作。当用户在思考或者输入数据时,计算机就在等待。显然,这样效率会很低下,因为很多时候,计算机处于等待用户输入的状态。
那么,能不能把一系列需要操作的指令预先写下来,形成一个清单,然后一次性交给计算机,计算机不断地去读取指令来进行相应的操作?就这样, 批处理操作系统 诞生了。用户可以将需要执行的多个程序写在磁带上,然后交由计算机去读取并逐个地执行这些程序,并将输出结果写到另一个磁带上。
虽然批处理操作系统的诞生极大地提高了任务处理的便捷性,但是仍然存在一个很大的问题:
假如有两个任务 A 和 B,任务A 在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。人们于是想,能否在 任务A 读取数据的过程中,让 任务B 去执行,当 任务A 读取完数据之后,让 任务B 暂停,然后让 任务A 继续执行?
但是这样就有一个问题,原来每次都是一个程序在计算机里面运行,也就说内存中始终只有一个程序的运行数据。而如果想要 任务A 执行 I/O操作 的时候,让 任务B 去执行,必然内存中要装入多个程序,那么如何处理呢?多个程序使用的数据如何进行辨别呢?并且,当一个程序运行暂停后,后面如何恢复到它之前执行的状态呢?
这个时候,人们就发明了进程,用进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。并且,进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂停时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。
这就是并发,能够让操作系统从宏观上看起来同一个时间段有多个任务在执行。换句话说,进程让操作系统的并发成为了可能。注意,虽然并发从宏观上看有多个任务在执行,但是事实上,任一个具体的时刻,只有一个任务在占用CPU资源(当然是对于单核CPU来说的)。
(2). 为什么会出现线程?
在出现了进程以后,操作系统的性能得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对 实时性 有了要求。因为一个进程在一个时间段内只能做一件事情,如果一个进程有多个子任务,只能逐个地去执行这些子任务。比如,对于一个监控系统来说,它不仅要把图像数据显示在画面上,还要与服务端进行通信获取图像数据,还要处理人们的交互操作。如果某一个时刻该系统正在与服务器通信获取图像数据,而用户又在监控系统上点击了某个按钮,那么该系统就要等待获取完图像数据之后才能处理用户的操作,如果获取图像数据需要耗费 10s,那么用户就只有一直等待。显然,对于这样的系统,人们是无法满足的。
那么,可不可以将这些子任务分开执行呢?即,在系统获取图像数据的同时,如果用户点击了某个按钮,则会暂停获取图像数据,而先去响应用户的操作(因为用户的操作往往执行时间很短),在处理完用户操作之后,再继续获取图像数据。人们就发明了线程,让一个线程去执行一个子任务,这样一个进程就包括了多个线程,每个线程负责一个独立的子任务。这样,在用户点击按钮的时候,就可以暂停获取图像数据的线程,让 UI线程 响应用户的操作,响应完之后再切换回来,让获取图像的线程得到 CPU资源 。从而,让用户感觉系统是同时在做多件事情的,满足了用户对实时性的要求。
换句话说,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。但是要注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程 是操作系统进行资源分配的基本单位,而 线程 是操作系统进行调度的基本单位。
(3). 多线程并发
由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:如果多个线程要同时访问某个资源,怎么处理? 这个问题就是并发安全性问题。
此外,可能有朋友会问,现在很多时候都采用多线程编程,那么是不是多线程的性能一定就由于单线程呢?答案是不一定,要看具体的任务以及计算机的配置。比如说:对于单核CPU,如果是 CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直占用 CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。但是对于比如交互类型的任务,肯定是需要使用多线程的。而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。
虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。因此,在实际编程过程中,要根据实际情况具体选择。
二. 并发简史总结
早期的计算机不包含操作系统,它们从头到尾只执行一个程序,并且这个程序能够访问计算机中的所有资源。这对于昂贵且稀有的计算机资源来说是一种浪费;
操作系统的出现使得计算机能同时运行多个程序,不同的程序都在单独的进程中运行,并且操作系统为各个独立执行的进程分配资源( eg: 通过粗粒度时间分片使程序共享资源,如 CPU 等 )。这无疑提高了计算机资源的利用率;
在早期的分时系统中,每个进程的执行都是串行的。串行编程模型的优势在于其简单性和直观性,因为它每次只做一件事情,做完之后再做另一件。这种串行编程模型仍然存在着计算机资源利用率不高的问题;
促使进程出现的因素同样也促使着线程的出现。线程允许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,但每个线程都有各自的 程序计数器 、 栈 以及 局部变量 等等;
线程也被成为轻量级进程。在大多数现代操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协同机制,那么线程将彼此独立执行。由于同一个进程的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量,这就需要实现一种比进程间共享数据粒度更细的数据共享机制。如果没有明确的同步机制来协同对共享数据的访问,将造成不可预测的结果。
三. 线程的优势
- 解耦、简化程序开发
在程序中,如果我们为每种类型的任务都分配一个专门的线程,那么可以形成一种串行执行的假象,并将程序的执行逻辑与调度机制的细节,交替执行的操作,异步 I/O 以及资源等待等问题分离开来。通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。
Servlet 框架就是一个很好的例子。框架负责解决一些细节问题,包括请求管理、线程创建、负载平衡等,并在正确的时刻将请求分发给正确的应用程序组件(对应的一个具体Servlet)。编写 Servlet 的开发人员不需要了解有多少请求在同一时刻被处理,也不需要了解套接字的输入(出)流是否被阻塞。当调用 Servlet 的 service 方法来响应 Web请求时,可以以同步方式来处理这个请求,就好像它是一个单线程的程序。这种方式简化了组件的开发,大大降低框架学习门槛。
多线程还有助于用户界面的灵敏响应。例如,在 Android 开发中,我们常常将网路请求或 I/O 等耗时操作单独放到一个线程中,以提高响应的灵敏度。
- 提高资源利用率
多处理器系统的出现,使得同一个程序的多个线程可以被同时调度到多个 CPU 上运行。因此,多线程程序可以通过提高处理器资源的利用率来提升系统的吞吐率。其实,多线程程序也有助于在单处理器系统上获得更高的吞吐率(如果程序的一个线程在等待 I/O 操作的完成,另一个线程可以继续运行,使程序能够在 I/O 阻塞期间继续运行)。
四. 线程带来的风险
- 安全性问题
在线程安全性的定义中,最核心的概念就是正确性。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
线程不安全类示例:
@NotThreadSafe
public class UnsafeSequence {
private int value; /** Returns a unique value. */
public int getNext() {
return value++;
}
}
虽然 递增运算 “value++” 看上去是单个操作,但实际上它包含三个独立的操作:读取 value, 将 value 加 1,并将计算结果写入 value。由于运行时各个线程执行顺序的不确定性,可能这段代码在不同线程的调用中返回相同的数值
- 活跃性问题
活跃性问题关注的是:某件正确的事情最终会发生。导致活跃性的问题包括死锁、饥饿等。
- 性能问题
性能问题关注的是:正确的事情能够尽快发生。性能问题包括多个方面,例如响应不灵敏,吞吐率过低,资源消耗过高等。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁出现上下文切换操作(Context Switch),这种操作会导致 CPU 时间更多的花在线程调度上而非线程的运行上。
五. 线程无处不在
在 Java 中,一个应用程序对应着一个JVM实例(JVM进程)。Java采用的是 单线程编程模型 ,即在我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程。但是要注意,虽然只有一个线程来执行任务,不代表JVM中只有一个线程,JVM实例在创建的时候,同时会创建很多其他的线程(比如垃圾收集器线程)。由于Java采用的是单线程编程模型,因此在进行UI编程时要注意将耗时的操作放在子线程中进行,以避免阻塞主线程(在UI编程时,主线程即UI线程,用来处理用户的交互事件)。
public class Test {
public static void main(String[] args) {
// 获取运行当前代码的线程的名字
String curThreadName = Thread.currentThread().getName();
System.out.println(curThreadName);
}
}/* Output:
main
*/
总结;
进程是对运行时程序的封装,可以保存程序的运行状态,实现操作系统的并发;
线程是进程的子任务,保证程序的实时性;
进程是操作系统资源的分配单位,线程是CPU调度的基本单位;
进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。
Java并发的背景的更多相关文章
- Java并发编程-看懂AQS的前世今生
在具备了volatile.CAS和模板方法设计模式的知识之后,我们可以来深入学习下AbstractQueuedSynchronizer(AQS),本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...
- (转)Java并发编程:线程池的使用
背景:线程池在面试时候经常遇到,反复出现的问题就是理解不深入,不能做到游刃有余.所以这篇博客是要深入总结线程池的使用. ThreadPoolExecutor的继承关系 线程池的原理 1.线程池状态(4 ...
- Java并发编程-阻塞队列(BlockingQueue)的实现原理
背景:总结JUC下面的阻塞队列的实现,很方便写生产者消费者模式. 常用操作方法 常用的实现类 ArrayBlockingQueue DelayQueue LinkedBlockingQueue Pri ...
- 百万并发中间件系统的内核设计看Java并发性能优化
“ 这篇文章,给大家聊聊一个百万级并发的中间件系统的内核代码里的锁性能优化. 很多同学都对Java并发编程很感兴趣,学习了很多相关的技术和知识.比如volatile.Atomic.synchroniz ...
- Java并发编程之深入理解线程池原理及实现
Java线程池在实际的应用开发中十分广泛.虽然Java1.5之后在JUC包中提供了内置线程池可以拿来就用,但是这之前仍有许多老的应用和系统是需要程序员自己开发的.因此,基于线程池的需求背景.技术要求了 ...
- Java并发编程的艺术,解读并发编程的优缺点
并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在 ...
- Java 并发:学习Thread 类
Java 中 Thread类 的各种操作与线程的生命周期密不可分,了解线程的生命周期有助于对Thread类中的各方法的理解.一般来说,线程从最初的创建到最终的消亡,要经历创建.就绪.运行.阻塞 和 消 ...
- Java 并发编程(一):摩拳擦掌
这篇文章的标题原本叫做——Java 并发编程(一):简介,作者名叫小二.但我在接到投稿时觉得这标题不够新颖,不够吸引读者的眼球,就在发文的时候强行修改了标题(也不咋滴). 小二是一名 Java 程序员 ...
- 一个故事搞懂Java并发编程
最近在给别人讲解Java并发编程面试考点时,为了解释锁对象这个概念,想了一个形象的故事.后来慢慢发现这个故事似乎能讲解Java并发编程中好多核心概念,于是完善起来形成了了这篇文章.大家先忘记并发编程, ...
随机推荐
- c语言:分治算法之大数相乘
我们把整数A由规模n分为n1和n2,把整数B由规模m分为m1和m2,如下图: 则A分为n1位的A1和n2位的A1,B分为m1位的B1和m2位的B2,如下式所示: 以此类推,我们可以把A1.A2.B1. ...
- RocketMQ基础概念剖析,并分析一下Producer的底层源码
由于篇幅原因,本次的源码分析只限于Producer侧的发送消息的核心逻辑,我会通过流程图.代码注释.文字讲解的方式来对源码进行解释,后续应该会专门开几篇文章来做源码分析. 这篇博客聊聊关于Rocket ...
- Pandas初体验
目录 Pandas 一.简介 1.安装 2.引用方法 二.series 1.创建方法 2.缺失数据处理 2.1 什么是缺失值 2.2 NaN特性 2.3 填充NaN 2.4 删除NaN 2.5 其他方 ...
- 《C++ Primer》笔记 第2章 变量和基本类型
如果你的数值超过了int表示范围,选用long long 如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char 执行浮点数运算选用double 当一 ...
- 剑指 Offer 63. 股票的最大利润 + 动态规划
剑指 Offer 63. 股票的最大利润 Offer_63 题目描述 方法一:暴力法 package com.walegarrett.offer; /** * @Author WaleGarrett ...
- vue+lib-flexible实现大小屏幕,超大屏幕的适配展示。
p.p1 { margin: 0; font: 12px "PingFang SC" } span.s1 { font: 12px "Helvetica Neue&quo ...
- vue全家桶和react全家桶
vue全家桶:vue + vuex (状态管理) + vue-router (路由) + vue-resource +axios +elementui react全家桶 : react + re ...
- 最权威的html 标签属性大全
<p>---恢复内容开始---</p>1.html标签 <marquee>...</marquee>普通卷动 <marquee behavior= ...
- P1303_A*B Problem(JAVA语言)
思路:BigInteger 三杀! //四行搞定 题目描述 求两数的积. 输入输出格式 输入格式: 两行,两个数. 输出格式: 积 输入输出样例 输入样例#1: 复制 1 2 输出样例#1: 复制 2 ...
- x86汇编 条件跳转
条件跳转表 汇编语言-条件跳转指令 直接转移指令 指令格式 机器码 测试标志 条件说明 符号 JO OPR 70 OF=1 结果有溢出 JNO OPR 71 OF= ...