1)线程概述
文章目录
为什么有了进程还要有线程?上一节的思路很明显的理解到了,因为需要解决计算机中程序间并行的问题,所以提出进程作为操作的单位,从逻辑的角度,其实进程就是一组CPU状态的组合,不管是记录了内存的状态还是CPU当时的状态,进程都是作为一个综合的单位元素去处理的。
传统的操作系统中,进程有一个地址空间和控制线程,这个几乎就是进程的定义,控制线程可以直接进程程序的各个部分的操作,同时也可以用于去组织管理其他进程中的线程为身份。
怎么突然就引入线程组织管理的作用了?
从另一个角度来谈,一个程序中也可以用进程的思想来进行效率的提高,一个进程有自己的独立的内存空间,而操作内存空间的操作可能是将数据从磁盘中读入,也可能是将操作系统内存空间中的数据进行拷贝,也可能是进行大量的计算。
**很正常的思想这些工作交给一个人可以吗?**当然可以,并且每个独立的个体都有独立完成这些任务的能力,因为运行的本质CPU运行指令的能力是固定的,但是一个人完成就会有一个人必须串行执行或者完成一个的同时另一个就必须等待的问题,如果由不同的人去完成效率就会提高很多。
那这里就又有一个问题了,**我一个CPU的情况下,无论多少个任务本质上都是一个人来完成的,也就是很多人的本质也是一个人,那么对一个程序这样做的意义还有什么呢?**我的理解是任务切换是需要代价的,有些任务必须得同时进行并同时完成,这个时候就需要“伪并行”的思想来运作,而这个在进程里面必须要解决的并行的问题就需要一个单位,而保存进程若干工作状态的实体也自然而然出来了,叫做线程。
1.1线程使用的原因
- 在进程内部需要满足并行:
- 在进程当中,并行实体需要具有共享同一个地址空间和可用数据的能力,而这种能力是多进程模型(因为不同的进程之间具有不同的地址空间)所无法表达的。
- 线程比进程更轻量级
- 创建、销毁、切换进程所需要消耗的资源远比线程要大的多
- 并行需要频繁的切换,那么线程无疑是必然出现的选择
- 从性能的角度
- 如果所有的线程都是CPU密集型(频繁的使用CPU而不是其他硬件设备),那么CPU利用率高这不需要过多的讨论,但是如果程序过程中需要大量的计算和IO处理,多个线程重叠进行,通过这种方式提高CPU的利用率就能很大的提高CPU的利用率,CPU是最快的设备,那么性能的提高也不需要过的讨论
- 实现真正的并行
- 之前考虑的角度都是从单核的角度,如果是多核的角度,就能实现真正的并行
简单一点总结出使用线程的原因就是:
必须性:
在一些情况下,必须通过多线程的手段程序在进行和一方进行交互的同时又能串行的执行自己的逻辑
好处:
使用多线程可以显著的提高性能
1.2使用线程提供其他任务运行机会
Runnable r = ()->{
task code;
}
Thread t = new Thread(r);
t.start;
通过创建线程来进行固定任务代码的执行,举个非常鲜明的例子就是:
在聊天工具中,往往会有一个线程不断的去检测是否有人给我发送信息,而同时程序又需要处理我想要发出去的信息,如果通过一个主线程去完成的话,当别人给我发消息的时候我无法输入,无法回车,甚至无法关闭聊天框,因为在一个main中这些相关的内容都是串行运行的,很显然这不是非常不利于用户体验的,这个时候我就可以用一个子线程去处理别人给我发的消息,当有人给我发消息时,这个子线程通知主线程去拿消息,或者直接将消息送过来显示,而我的主线程仍然处理我自己的使用。
还用另一种创建线程的方式:
public class MyThread extends Thread{
public void run(){
task code;
}
}
通过去构建一个子类对象,调用start方法,这样不利于**运行机制和并行运行任务的解耦**,如果有很多的任务的情况下,为每个任务创建一个线程很显然代价太大了,也可以通过线程池来解决这个问题。
1.3中断线程
什么叫线程的中断,从字面意思上来讲就是线程被打断了,但是很明显在字面意思上并没有说线程被打断了就会引起线程的终止。
线程终止的条件:
- run方法执行方法体中最后一条语句,并经由return 语句返回
- 出现了方法中没有捕获的异常
- 通过被弃用的方法stop
- interrupt 方法中断异常,请求线程终止
从上面这几种情况来说,并没有强制异常终止的方法,而我们主要讨论的就是如何通过intterupt方法来终止异常。
interrupt方法的原理:
调用interrupt 方法时,线程的中断状态将被置位,中断状态在Java中的表现就是一个boolean 标志,每个线程都应该时不时的检查这个标志,判断线程是否被中断。
如何判断线程中断:
while(!THread.currentThread().isInterrupted() && more work to do){
do more work;
}
需要注意的是:
- 如果线程被阻塞,无法检测中断状态
- 当一个被阻塞的线程(调用了sleep 或 wait 方法),阻塞调用将会被Interrtupted Exception异常中断
处理中断:
中断的目的往往并不是为了终止一个线程,而是引起这个线程的注意,被中断线程可以决定如何去响应线程,可以通过处理异常,继续运行,不理会中断,当然也可以通过中断来终止线程当前的运行,也就是通过中断终止线程
Runnale r = ()->{
try{
...
while(!THread.currentThread().isInterrupted() && more work to do){
do more work;
}
}catch(InterruptedException e){
//Thread was interrupted during sleep or wait
}finally{
clean up,if required;
}
//exiting the run method terminates the thread
}
如果每次工作迭代之后都调用sleep方法,或者其他可中断的方法,isInterrupted 检测是完全没有必要的
这里有两个非常相似的方法:
interrupted方法和isInterrupted 方法
- interrupted 方法用于检测当前的线程是否被中断,而且会清楚该线程的中断状态
- isInterrupted 也用于检测,但是并不会改变中断状态
很多代码中会发现InterruptedException异常被抑制在很低的层次(几乎不去做处理),例如:
void test(){
try{
sleep(delay);
}catch(InterruptedException e){}
}
并不建议这样做,更推荐两种处理InterruptedException 的方法是
void test(){
try{
sleep(delay);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
或者更好的选择是抛出这个异常,让调用者去处理和捕获这个异常
void test() throws InterruptedException{
sleep(delay);
}
1.4线程状态
在Java中线程有六个状态
- New新创建
- new Thread®代码执行时就会创建对应的线程,这时候程序还没有运行线程的task代码,也就意味着还可以在运行前进行一些基础工作
- Runnable 可运行
- 一旦调用了start 方法,线程就处于可运行状态,这里Java并没有将运行中作为一种新的状态,也就是一个正在运行的线程仍然是处于可运行的状态
- (我对这里的理解就是,java 并不关心从就绪态到运行态转换的过程,或者说大多数高级语言都不会很关心这个过程,因为不管是线程调度器还是线程管理上很多权限都是交由操作系统进行管理,Java并不是一门十分底层的语言,这里也是体现出的一个点)
- 操作系统对于线程调度算法往往有两种一种是抢占式的一种是协调式的,目前的计算机往往都是抢占式的线程调度,因为很多情况下要保证使用者的体验,抢占式的很明显更加的满足需求,而协调式的往往用到一些小型设备中:协调式的含义就是,一个线程只有调用yield 方法,或者被阻塞或者等待式,线程才会主动的丧失cpu控制权
- 如果计算机是多处理器的,就可以在此基础上实现并行
- Blocked 被阻塞
- Waiting 等待
- Time waiting 计时等待
- 但线程处于被阻塞和被等待的状态时,它会暂时不活动,不运行任何代码且消耗最少的资源,直到线程调度重新激活它,而这里更为关注的是**怎样到达一种非活动的状态**
- 当一个线程试图持有一个内部的对象锁,而不是java.util.concurrent 中的锁,而这个锁被其他线程持有,那么这个线程就会阻塞,直到持有锁的线程释放了锁,并且线程调度器允许这个线程持有锁
- 线程等待另一个线程通知调度器一个条件时,这个线程就会进入等待状态(之后我们会详细介绍,而这里需要明白的是等待状态和阻塞状态是有很大区别的)
- 有几个带超时参数的方法,调用这些方法会导致线程进入计时等待的状态
- Terminated 被终止
- 因为run方法正常退出而自然死亡
- 因为一个没被捕获的异常终止了run方法而死亡
- stop方法抛出ThreadDeath 错误对象由此杀死进程
1.5线程优先级
现在操作系统往往在线程/进程调度算法上面理论基础或者本身实现都是使用的时间片轮转算法和抢占式算法,为这样的算法提供前提的一种方式就是设置线程的优先级,对于线程的优先级是非常好理解的,优先级高的线程将会获取更多的时间片(也就是运行时间),而相对来说优先级低的线程会获取更少的时间片,甚至饿死,引入线程优先级的功能实现我理解上一方面是对操作系统的一种映射关系,另一方面是为了实现和解决特殊功能.
这里需要注意的几个点就是:
- 可以通过setPriority 方法设置线程的优先级
- 默认NORM_PRIORITY 5
- 最高MAX_PRIORITY 10
- 最低MIN_PRIORITY 1
- Java线程的优先级依赖于宿主机平台的线程实现机制,相应的就会造成线程更多或者更少
- windows7个优先级
- linux中,java的优先级被忽略,所有线程具有相同的优先级
- 在根据线程优先级编写的程序上,程序的实现逻辑不能过度依赖于优先级,如果高优先级的线程没有进入非活动状态,低优先级的线程永远不能执行,因为系统的线程调动器总是从高优先级的线程中进行调度的,这样可能会导致有些低优先级线程饿死(一直无法分配到CPU时间片导致无法运行)
-
2)API
java.lang.Thread
-
static void sleep(long millis)
- 休眠指定的毫秒数
-
Thread(Runnable r)
- 构建新的线程,并在start 开始时调用给定目标的run()方法
-
void start()
- 启动这个线程,并调用run()方法,这个方法将立即返回,并且新的线程将并发运行
-
void interrupt()
- 向线程发送中断请求
- 线程中断状态设置为true
- 如果该线程被sleep阻塞,则抛出InterruptedException
-
static boolean interrupted()
- 测试线程是否被中断
- 会讲中断状态重置为false
-
boolean isInterrupted()
- 测试线程是否被终止
-
static Thread currentThread()
- 返回当前执行线程的Thread对象
-
void join()
- 等待终止指定的线程
-
void join(long millis)
- 等待指定的线程死亡或者经过指定的毫秒数
-
Thread.State getState()
- 得到线程的状态
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
-
void setPriority(int newPriority)
- 设置线程的优先级,1~10之间
-
static int MIN_PRIORITY
- 线程最小优先级,值为1
-
static int NORM_PRIORITY
- 线程默认优先级,值为5
-
static int MAX_PRIORITY
- 线程最高优先级,值为10
-
static void yield()
- 使当前线程处于让步状态。如果其他的可运行线程具有至少与此线程同样高的优先级(必要条件),那么这些线程接下来会被调度
-
void setDaemon(boolean isDaemon)
- 标识该线程为守护线程或者用户线程,这一方法必须在线程启动前进行调用
java.lang.Runnable
- void run()
- 必须覆盖
- 并且在方法中提供所要执行的任务指令