初探线程之基础篇

本文详细介绍了进程和线程的概念,指出进程是资源分配单位,线程是CPU调度单位。CPU时间片轮转机制使得在单核CPU上也能实现多线程的并发执行。并行和并发的区别在于是否真正同时执行,而并发编程可以提高系统资源利用率和响应速度。文章还讨论了Java中线程的启动与中止,强调了线程安全、死锁和线程优先级等问题,并探讨了线程调度和守护线程的应用。
摘要由CSDN通过智能技术生成


什么是进程和线程

进程是程序运行资源分配的最小单位

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就 是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

线程是 CPU 调度的最小单位,必须依赖于进程而存在

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中 必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其 他的线程共享进程所拥有的全部资源。

线程无处不在

任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异 步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都 离不开线程和并发的知识。

CPU 核心数和线程数的关系

在这里插入图片描述

多核心:也指单芯片多处理器( Chip Multiprocessors,简称 CMP),CMP 是由美国 斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成 到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地 运行程序是实现超高速计算的一个重要方向,称为并行处理。

多线程: Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线 程同步执行并共享处理器的执行资源。多线程: Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线 程同步执行并共享处理器的执行资源。

核心数、线程数:目前主流 CPU 都是多核的。增加核心数目就是为了增加线 程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也 就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程 数形成 1:2 的关系。

CPU 时间片轮转机制

我们平时在开发的时候,感觉并没有受 cpu 核心数的限制,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?这是因为操作系统提供了一种 CPU 时间片轮转机制。

时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则 CPU 当即进行切换。调度程序所要做的 就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一 个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要 5ms, 再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行 进程切换。CPU 时间的 20%被浪费在了管理开销上了。

为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的 进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要5s才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发生。

结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常 是一个比较合理的折衷。

在 CPU 死机的情况下,其实大家不难发现当运行一个程序的时候把 CPU 给弄 到了 100%再不重启电脑的情况下,其实我们还是有机会把它 KⅢ掉的,我想也正是 因为这种机制的缘故。

澄清并行和并发

我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车 辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可 以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线 程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而 每个高速公路并排有多个车道。

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。

俗话说,一心不能二用,这对计算机也一样,原则上一个 CPU 只能分配给一个 进程,以便运行这个进程。我们通常使用的计算机中只有一个 CPU,也就是说只有 一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术 相当复杂,最容易理解的是“时间片轮转进程调度算法”。

综合来说:

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

并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行。
两者区别:一个是交替执行,一个是同时执行。

并发编程的意义

充分利用 CPU 的资源

加快响应用户的时间

可以使你的代码模块化,异步化,简单化

多线程程序需要注意事项

线程之间的安全性

线程之间的死锁

线程太多了会将服务器资源耗尽形成死机宕机

Java 天生就是多线程的

一个 Java 程序从 main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行 main() 方法的是一个名称为 main 的线程。

[1] main //main 线程,用户程序入口

[2] Reference Handler//清除 Reference 的线程

[3] Finalizer // 调用对象 finalize 方法的线程

[4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程

[5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等

[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的

线程的启动与中止

启动线程的方式有:

1、X extends Thread;,然后 X.start

2、X implements Runnable;然后交给 Thr
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

Thread 和 Runnable 的区别:

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

中止线程:

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

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

中断线程:

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

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

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

不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。这种情况下,使用中断会更好,因为:
1、一般的阻塞方法,如 sleep 等本身就支持中断的检查。
2、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。

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

深入理解线程的方法和应用

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。

start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。

而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用。

其他的线程相关方法和应用

yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也 不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( )的线 程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。

所有执行 yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

join 方法:

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B。

线程的优先级

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

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

线程的调度

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

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

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

在 Java 中,Thread.yield()可以让出 CPU 执行时间,但是对于获取,线程本身是没有办法的。对于获取 CPU 执行时间,线程唯一可以使用的手段是设置线程 优先级,Java 设置了 10 个级别的程序优先级,当两个线程同时处于 Ready 状态 时,优先级越高的线程越容易被系统选择执行。

Java 中的线程优先级是通过映射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,操作系统中线程的优先级有时并不能和 Java 中的 一一对应,所以 Java 优先级并不是特别靠谱。

所以在面试中如果遇到相关的问题,可以这样回答:Java 中的线程是通过映 射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,而操 作系统级别,OS 是以抢占式调度线程,我们可以认为线程是抢占式的。Java 虚 拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如 果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处 于运行状态的线程会一直运行,直至它不得不放弃 CPU。而且操作系统中线程的 优先级有时并不能和 Java 中的一一对应,所以 Java 优先级并不是特别靠谱。但 是在 Java 中,因为 Java 没有提供安全的抢占式方法来停止线程,要安全的停止 线程只能以协作式的方式。

守护线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的 时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。

Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值