文章目录
一.并发编程的目的
1.加快响应用户的时间
2.使代码模块化,异步化,简单化
3.充分利用 CPU 的资源
二.进程和线程
进程
我们常听说的是应用程序,也就是 app,由指令和数据组成。但是当我们不
运行一个具体的 app 时,这些应用程序就是放在磁盘(也包括 U 盘、远程网络
存储等等)上的一些二进制的代码。一旦我们运行这些应用程序,指令要运行,
数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中
还需要用到磁盘、网络等设备,从这种角度来说,进程就是用来加载指令、管理
内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个
进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程
(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如
网易云音乐、360 安全卫士等)。显然,程序是死的、静态的,进程是活的、动态
的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进
程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由
你启动的进程。
线程
一个机器中肯定会运行很多的程序,CPU 又是有限的,怎么让有限的 CPU
运行这么多程序呢?就需要一种机制在程序之间进行协调,也就所谓 CPU 调度。
线程则是 CPU 调度的最小单位。
线程必须依赖于进程而存在,线程是进程中的一个实体,是 CPU 调度和分
派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不
拥有系统资源,,只拥有在运行中必不可少的资源(如程序计数器,一组寄存器和栈),
但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个进程可
以拥有多个线程,一个线程必须有一个父进程。线程,有时也被称为轻量级进程
(Lightweight Process,LWP),早期 Linux 的线程实现几乎就是复用的进程,后来
才独立出自己的 API。
Java 线程的无处不在
Java 线程的无处不在
Java 中不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里
面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制,远程访问接口 RM 等,
任何一个监听事件,onclick 的触发事件等都离不开线程和并发的知识。
进程间的通信
-
管道,分为匿名管道(pipe)及命名管道(named pipe):匿名管道可用
于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外,
它还允许无亲缘关系进程间的通信。 -
信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较
复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器
收到一个中断请求效果上可以说是一致的。 -
消息队列(message queue):消息队列是消息的链接表,它克服了上两
种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息
队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。 -
共享内存(shared memory):可以说这是最有用的进程间通信方式。它
使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共
享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。 -
信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间
得同步和互斥手段。 -
套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络
中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用
Unix domain socket(比如同一机器中 MySQL 中的控制台 mysql shell 和 MySQL 服
务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验
和、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高。
CPU 核心数和线程数的关系
在超线程技术加持下,cpu核心数与线程数形成 1:2 的关系。
上下文切换
既然操作系统要在多个进程(线程)之间进行调度,而每个线程在使用 CPU
时总是要使用 CPU 中的资源,比如 CPU 寄存器和程序计数器。这就意味着,操
作系统要保证线程在调度前后的正常执行,所以,操作系统中就有上下文切换的
概念,它是指 CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
上下文是 CPU 寄存器和程序计数器在任何时间点的内容。
寄存器是 CPU 内部的一小部分非常快的内存(相对于 CPU 内部的缓存和 CPU
外部较慢的 RAM 主内存),它通过提供对常用值的快速访问来加快计算机程序的
执行。
程序计数器是一种专门的寄存器,它指示 CPU 在其指令序列中的位置,并
保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系
统。
并行和并发
并发 Concurrent:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多
线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉
到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算
机的速度太快,我们无法察觉到而已.
并行 Parallel:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边
打电话,这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行
Java 程序天生就是多线程的
一个 Java 程序从 main()方法开始执行,然后按照既定的代码逻辑执行,看
似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行 main()
方法的是一个名称为 main 的线程。
而一个 Java 程序的运行就算是没有用户自己开启的线程,实际也有有很多
JVM 自行启动的线程一个 Java 程序从 main()方法开始执行,然后按照既定的代码逻辑执行,看
似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行 main()
方法的是一个名称为 main 的线程。
而一个 Java 程序的运行就算是没有用户自己开启的线程,实际也有有很多
JVM 自行启动的线程。
三 .线程的启动与中止
线程的启动
Java 中有两种方式创建一个线程用以执行,一种是派生自
Thread 类,另一种是实现 Runnable 接口。
当然本质上 Java 中实现线程只有一种方式,都是通过 new Thread()创建线程
对象,调用 Thread#start 启动线程。
至于基于 callable 接口的方式,因为最终是要把实现了 callable 接口的对象
通过 FutureTask 包装成 Runnable,再交给 Thread 去执行,所以这个其实可以和
实现 Runnable 接口看成同一类。
而线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关
系。
所以,比较赞同官方的说法,有两种方式创建一个线程用以执行
中止
线程自然终止
要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
深入理解 run()和 start()
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()
其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。
只有执行了 start()方法后,才实现了真正意义上的启动线程。
从 Thread 的源码可以看到,Thread 的 start 方法中调用了 start0()方法,而
start0()是个 native 方法,这就说明 Thread#start 一定和操作系统是密切相关的。
start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现
的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常(注意,此处
可能有面试题:多次调用一个线程的 start 方法会怎么样?)。
而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方
法并没有任何区别,可以重复执行,也可以被单独调用。
线程的状态/生命周期
Java 中线程的状态分为 6 种:
- 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。
- 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种
状态笼统的称为“运行”。
线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。
该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权,
此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中
状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作
(通知或中断)。 - 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时
间后自行返回。 - 终止(TERMINATED):表示该线程已经执行完毕。
其他的线程相关方法
yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也
不会释放锁资源。同时执行 yield()的线程有可能在进入到就绪状态后会被操作系
统再次选中马上又被执行。
线程的优先级
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范
围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认
优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较
高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的
优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会
存在差异,有些操作系统甚至会忽略对线程优先级的设定。
线程的调度
线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种:
协同式线程调度(Cooperative Threads-Scheduling)
抢占式线程调度(Preemptive Threads-Scheduling)
使用协同式线程调度的多线程系统,线程执行的时间由线程本身来控制,线
程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。使用协同
式线程调度的最大好处是实现简单,由于线程要把自己的事情做完后才会通知系
统进行线程切换,所以没有线程同步的问题,但是坏处也很明显,如果一个线程
出了问题,则程序就会一直阻塞。
使用抢占式线程调度的多线程系统,每个线程执行的时间以及是否切换都由
系统决定。在这种情况下,线程的执行时间不可控,所以不会有「一个线程导致
整个进程阻塞」的问题出现。
Java 线程调度就是抢占式调度,为什么?后面会分析。
在 Java 中,Thread.yield()可以让出 CPU 执行时间,但是对于获取执行时间,
线程本身是没有办法的。对于获取 CPU 执行时间,线程唯一可以使用的手段是
设置线程优先级,Java 设置了 10 个级别的程序优先级,当两个线程同时处于
Ready 状态时,优先级越高的线程越容易被系统选择执行。
线程和协程
为什么 Java 线程调度是抢占式调度?这需要我们了解 Java 中线程的实现模
式。
我们已经知道线程其实是操作系统层面的实体,Java 中的线程怎么和操作系
统层面对应起来呢?
任何语言实现线程主要有三种方式:使用内核线程实现(
1:1 实现),使用用
户线程实现(1:N 实现),使用用户线程加轻量级进程混合实现(N:M 实现)。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调
度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的
时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置
为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线
程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中
的内容来确保执行关闭或清理资源的逻辑。
线程间的通信和协调、协作
很多的时候,孤零零的一个线程工作并没有什么太多用处,更多的时候,我
们是很多线程一起工作,而且是这些线程间进行通信,或者配合着完成某项工作,
这就离不开线程间的通信和协调、协作。
管道输入输出流
我们已经知道,进程间有好几种通信机制,其中包括了管道,其实 Java 的
线程里也有类似的管道机制,用于线程之间的数据传输,而传输的媒介为内存。
synchronized 内置锁
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码
一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,
那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,
包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java 支持多个线程同时访问一个对象或者对象的成员变量,但是多个线程同
时访问同一个变量,会导致不可预料的结果。关键字 synchronized 可以修饰方法
或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一
个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使
多个线程访问同一个变量的结果正确,它又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态
方法或者一个类的 class 对象上的。
join()
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续
执行线程 B 剩下的代码。
synchronized 内置锁
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码
一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,
那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,
包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java 支持多个线程同时访问一个对象或者对象的成员变量,但是多个线程同
时访问同一个变量,会导致不可预料的结果。关键字 synchronized 可以修饰方法
或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一
个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使
多个线程访问同一个变量的结果正确,它又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态
方法或者一个类的 class 对象上的。