Java线程那些事

并发编程的特点

  • 加快响应用户的时间

比如我们经常用迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载。为什么呢?因为多个线程比一个线程快啊

  • 可以让代码更加模块化、异步化、简单化

理我我们实现电商系统,下订单和给用户发送短信,邮件就可以进行拆分,将给用户发送短信,邮件这两个步骤独立为单个系统,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化

  • 可以充分利用CPU 的资源

比如我们平时坐地铁时,很多人做长线地铁的时候都在认真的看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍的时间。同样的,CPU也是一样,需要充分利用

基本概念

什么是进程

所谓进程就是我们经常听说的应用程序,也就是APP,有指令和数据组成。但是当我们不运行一个具体的APP 时,这些应用程序也就是放在磁盘或者U盘等地方上的一些二进制代码。一旦我们运行这些应用程序,指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行的过程中,还需要用到磁盘,网络等设备,从这个角度来说,进程就是用来加载指令、管理内存,管理IO。

当一个程序被运行起来,从磁盘加载这个程序的代码至内存,这就是开启了一个进程。进程可以视为程序的一个实例。大部分程序都是可以同时运行多个实例进程的。比如:记事本,画图等等等

什么是线程

在一台机器中肯定会运行很多程序,但是CPU 是有限的,怎么让有限的CPU 运行这么多程序呢?这时就需要一种机制在程序之间进行协调,也就是所谓的CPU调度。线程则是CPU 调度的最小单位。

线程依赖于进程而讯在,线程是进程中的一个实体,是CPU 调度和分派的基本单位。他比进程更小,能独立运行的基本单位。线程自己基本上不拥有系统的资源,只拥有在运行中必不可少的资源,比如(程序计数器,一组寄存器和栈)。但他可以与同属于一个进程的其他线程共享进程所拥有的全部资源。一个进程可以拥有多个线程,一个线程必须有一个父进程。线程:有时候也成为轻量级进程

进程之间是如何通信

  • 管道:分为匿名管道和命名管道。匿名管道可以用于具有亲缘关系的父子进程间的通信,命名管道处理具有匿名管道所有具有的功能外,还拥有无亲缘关系进程间的通讯。
  • 信号:信号在软件层次上对中断机制的一种模拟,他是一种比较复杂的通讯方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
  • 消息队列:消息队列是消息的连接表。他克服了上面两种通讯方式中信号量有限的缺点,具有写权限的进程可以按照一定得规则向消息队列中添加新的消息。对消息队列有读权限的进程可以从消息队列中读取消息。
  • 共享内存:可以说这是最有用的进程间的通讯方式。它使得多个进程可以访问同一个内存空间,不同进程可以及时看到对方进程中共享内存中数据的更新,这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  • 信号量:主要用于进程之间及同一种进程的不同线程之间的同步和互斥手段。
  • 套接字:这是一种更为一般得进程之间的通讯机制。它可用于网络中不同机器之间的进程间的通讯,应用非常广泛。同一机器中的进程还可以使用Unix domain Socket (例如:同一机器中Mysql中的控制Mysql shell,和Mysql 服务程序中的链接。)这种方式不需要经过网络协议栈,不需要拆包打包、计算校验、维护序号和应答等等,比纯粹基于网络的进程间通讯肯定效率更高。

并行和并发

什么是并发

所谓并发(Concurrent)是指应用能够交替执行不同的任何,比如单核CPU执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,以达到”同时执行效果“其实并不是的,只是计算机的速度太快,我们无法察觉而已

什么是并行

并行(Parallel)是指应用能够同时执行不同的任务,例如:吃饭的时候可以边打电话便吃饭,这两件事可以同时执行。

两者的区别:一个是交替执行(并发),一个是同时执行(并行)

线程的启动方式

  1. 新启线程有几种方式

按照Java源码中Thread的注释,只有两种,一种是派生继承自Thread类的,一种是实现Runnable接口

下面是答案的来源:

按照官网上的说,Java中实现线程的方式只有一种,都是通过 new Thread() 创建线程对象,调用Thread#start 启动线程。至于基于Callable 接口的方式,最终是要把实现了callable接口的对象通过FutureTask包装成Runnable,再交给Thread 去执行,所以这个其实可以和实现Runnable接口看成同一类

  1. Thread 和Runnable 的区别

Thread 才是Java里对线程的唯一抽象对象。Runnable 只是对任务的抽象,Thread 可以接受任意一个Runnable 的实例并执行。

  1. **Callable ,Future 和FutureTask **

Runnable 是一个接口, 在它里面只声明了一个 run()方法,由于 run()方法返 回值为 void 类型,所以在执行完任务之后无法返回任何结果。

Callable 位于 java.util.concurrent 包下, 它也是一个接口, 在它里面也只声明 了一个方法,只不过这个方法叫做 call() ,这是一个泛型接口, call()函数返回的 类型就是传递进来的 V 类型。

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查 询是否完成、获取结果。必要时可以通过 get 方法获取执行结果, 该方法会阻塞 直到任务返回结果。

线程中止

  1. 自然终止

要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

  1. stop

暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend() resume() stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样, stop()方 法在终结一个线程时不会保证线程的资源正常释放, 通常是没有给予线程完成资 源释放工作的机会, 因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方 法。

  1. 中断

安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中 断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。

线程通过检查自身的中断标志位是否被置为 true 来进行响应,

线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。

如果一个线程处于了阻塞状态(如线程调用了 thread.sleep 、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。

不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调 用时会无法很快检测到取消标志, 线程必须从阻塞调用返回后, 才会检查这个取 消标志。这种情况下,使用中断会更好,因为,

一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,

二、检查中断位的状态和检查取消标志位没什么区别, 用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

线程的生命周期

线程的生命周期如下图:

Java 中线程的状态分为 6 种

  1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。
  2. 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种 状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。 该状态的线程位于可运行线程池中, 等待被线程调度选中, 获取 CPU 的使用权, 此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中 状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作 (通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时 间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程的优先级

在 Java 线程中, 通过一个整型成员变量 priority 来控制优先级, 优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

设置线程优先级时, 针对频繁阻塞(休眠或者 I/O 操作) 的线程需要设置较 高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。

一般很少设置优先级

线程的调度

线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种: 协同式线程调度(Cooperative Threads-Scheduling)抢占式线程调度(Preemptive Threads-Scheduling)

使用协同式线程调度的多线程系统, 线程执行的时间由线程本身来控制, 线 程把自己的工作执行完之后, 要主动通知系统切换到另外一个线程上。使用协同 式线程调度的最大好处是实现简单, 由于线程要把自己的事情做完后才会通知系 统进行线程切换, 所以没有线程同步的问题, 但是坏处也很明显, 如果一个线程 出了问题,则程序就会一直阻塞。

使用抢占式线程调度的多线程系统, 每个线程执行的时间以及是否切换都由 系统决定。在这种情况下, 线程的执行时间不可控, 所以不会有「一个线程导致 整个进程阻塞」的问题出现。

线程和协程

为什么 Java 线程调度是抢占式调度?这需要我们了解 Java 中线程的实现模 式。

我们已经知道线程其实是操作系统层面的实体, Java 中的线程怎么和操作系 统层面对应起来呢?

任何语言实现线程主要有三种方式:使用内核线程实现(1:1 实现),使用用 户线程实现(1:N 实现) ,使用用户线程加轻量级进程混合实现(N:M 实现)。

内核线程的实现

使用内核线程实现的方式也被称为 1: 1 实现。 内核线程(Kernel-Level Thread , KLT) 就是直接由操作系统内核(Kernel , 下称内核) 支持的线程,这种线程由内核来完成线程切换, 内核通过操纵调度器(Scheduler) 对线程进 行调度, 并负责将线程的任务映射到各个处理器上。由于内核线程的支持, 每个线程都成为一个独立的调度单元, 即使其中某一 个在系统调用中被阻塞了, 也不会影响整个进程继续工作,相关的调度工作也不 需要额外考虑,已经由操作系统处理了。

局限性:

首先,由于是基于内核线程实现的,所以各种线程操作,如创建、 析构及同步,都需要进行系统调用。而系统调用的代价相对较高, 需要在用户 态(User Mode)和内核态(Kernel Mode)中来回切换。其次, 每个语言层面的 线程都需要有一个内核线程的支持, 因此要消耗一定的内核资源(如内核线程的 栈空间),因此一个系统支持的线程数量是有限的。

用户线程实现

严格意义上的用户线程指的是完全建立在用户空间的线程库上, 系统内核不 能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完 全在用户态中完成, 不需要内核的帮助。 如果程序实现得当, 这种线程不需要 切换到内核态, 因此操作可以是非常快速且低消耗的, 也能够支持规模更大的 线程数量, 部分高性能数据库中的多线程就是由用户线程实现的。

用户线程的优势在于不需要系统内核支援, 劣势也在于没有系统内核的支援, 所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度 都是用户必须考虑的问题, 而且由于操作系统只把处理器资源分配到进程, 那诸 如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问 题解决起来将会异常困难, 甚至有些是不可能实现的。 因为使用用户线程实现 的程序通常都比较复杂, 所以一般的应用程序都不倾向使用用户线程。Java 语言 曾经使用过用户线程,最终又放弃了。 但是近年来许多新的、以高并发为卖点 的编程语言又普遍支持了用户线程,譬如 Golang。

混合实现

线程除了依赖内核线程实现和完全由用户程序自己实现之外, 还有一种将 内核线程与用户线程一起使用的实现方式, 被称为 N:M 实现。 在这种混合实 现下, 既存在用户线程, 也存在内核线程。

用户线程还是完全建立在用户空间中, 因此用户线程的创建、 切换、 析构等操作依然廉价, 并且可以支持大规模的用户线程并发。

同样又可以使用内核提供的线程调度功能及处理器映射, 并且用户线程的系统调用要通过内核线程来完成。在这种混合模式中, 用户线程与轻量级进程的 数量比是不定的,是 N:M 的关系。

协程

Java出现协程的原因

随着互联网行业的发展,目前内核线程实现在很多场景已经有点不适宜了。 比如, 互联网服务架构在处理一次对外部业务请求的响应, 往往需要分布在不 同机器上的大量服务共同协作来实现, ,也就是我们常说的微服务, 这种服务细 分的架构在减少单个服务复杂度、 增加复用性的同时, 也不可避免地增加了服 务的数量, 缩短了留给每个服务的响应时间。这要求每一个服务都必须在极短 的时间内完成计算, 这样组合多个服务的总耗时才不会太长;也要求每一个服 务提供者都要能同时处理数量更庞大的请求, 这样才不会出现请求由于某个服 务被阻塞而出现等待。

Java 目前的并发编程机制就与上述架构趋势产生了一些矛盾, 1:1 的内核 线程模型是如今 Java 虚拟机线程实现的主流选择, 但是这种映射到操作系统上 的线程天然的缺陷是切换、调度成本高昂, 系统能容纳的线程数量也很有限。 以 前处理一个请求可以允许花费很长时间在单体应用中, 具有这种线程切换的成本 也是无伤大雅的, 但现在在每个请求本身的执行时间变得很短、 数量变得很多 的前提下, 用户本身的业务线程切换的开销甚至可能会接近用于计算本身的开销, 这就会造成严重的浪费。

另外我们常见的 Java Web 服务器,比如 Tomcat 的线程池的容量通常在几十 个到两百之间, 当把数以百万计的请求往线程池里面灌时, 系统即使能处理得 过来,但其中的切换损耗也是相当可观的。

这样的话,对 Java 语言来说,用户线程的重新引入成为了解决上述问题一 个非常可行的方案。

其次, Go 语言等支持用户线程等新型语言给 Java 带来了巨大的压力, 也使 得 Java 引入用户线程成为了一个绕不开的话题。

协程简介:

协程的主要优势是轻量, 无论是有栈协程还是无栈协程, 都要比传统内核 线程要轻量得多。如果进行量化的话, 那么如果不显式设置,则在 64 位 Linux 上 HotSpot 的线程栈容量默认是 1MB ,此外内核数据结构(Kernel Data Structures) 还会额外消耗 16KB 内存。与之相对的, 一个协程的栈通常在几百个字节到几KB 之间, 所以 Java 虚拟机里线程池容量达到两百就已经不算小了, 而很多支 持协程的应用中, 同时并存的协程数量可数以十万计。

面试题

  1. 现在有 T1、T2、T3 三个线程, 你怎样保证 T2 在 T1 执行完后执行, T3 在 T2 执行完后执行?

答:用 Thread#join 方法即可, 在 T3 中调用 T2.join,在 T2 中调用 T1.join。

  1. 为什么 wait 和 notify 方法要在同步块中调用?

主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。 其实真实原因是:

这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下 出现。

假如我们有两个线程, 一个消费者线程, 一个生产者线程。生产者线程的任 务可以简化成将 count 加一,而后唤醒消费者; 消费者则是将 count 减一,而后 在减到 0 的时候陷入睡眠:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Monkey@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值