Java多线程相关
一、多线程基础
1. 进程和线程
1.1定义
- 进程是一系列指令和数据,或者是一段时间内对CPU等资源占有的集合,进程也可以认为是程序的一个实例(有些程序可以开多个实例,有些只能开启一个实例)
- 线程是指令流,最小调度单位。一个进程有一个或者多个线程。
1.2区别
- 进程基本独立,线程存在于进程内,或者说线程是进程的一个子集
- 线程可以共享资源,比如内存空间。一个例子是多个线程可以访问同一个共享变量
- 进程间通信,包括同一台计算机,以及不同计算机(需要通过网络,并且遵循同样的协议,比如HTTP)
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.3上下文切换
最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换
2. 并行和并发
- 并发是一个CPU在不同的时间去不同线程中执行指令。
- 并行是多个CPU同时处理不同的线程。
- 如何记住:并发,并行,串行。
3. 单核和多核
3.1同步异步:需要等待结果返回,才能继续运行就是同步;不需要等待结果返回,就能继续运行就是异步
3.2异步的例子:
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞
- tomcat 的工作线程 ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
3.3结论
- 单核 cpu 下,多线程不能实际提高程序运行效率
- 多核 cpu 可以并行跑多个线程(只是针对可以拆分的任务)
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
二、多线程创立*
1. 线程创立的方法
1.1 通过继承Thread类方法
1.2 通过Runnable 接口,并且配合Thread类(在创建线程时将Runnable实例作为参数传入该类的实例即可)。区别是这个方法可以将线程和任务分开。这个是比较推荐的。
1.3 通过FutureTask与Thread结合(同样是FutureTask实例作为参数传入,Thread的实例)。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以拿到返回结果。
1.4 Java中组合【以参数为中转】由于继承【Java是单继承】。
2. 线程原理
线程的运行:每个线程启动后,虚拟机都会为每个线程提供一个栈内存;每个栈内存里面都有一个栈内存,对应的方法调用所占的内存;每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
2.1 线程的上下文切换:(1)CPU时间片用完(2)垃圾回收,高级线程(3)线程自己调用方法,sleep(), wait()【会释放CPU资源】。当线程上下文切换发生时,需要操作系统会保存当前状态(程序计数器、虚拟机栈中每个栈帧的信息[如局部变量、操作数栈、返回地址等];程序计数器会记住下一条 jvm 指令的执行地址)。频繁发生线程的上下文切换会影响性能。
3. 线程常用方法
3.1 start 与 run:使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码(run 是在该线程中执行)
3.2 sleep 与 yield
sleep (使线程阻塞)
调用 sleep 会让当前线程从 jvm的Runable(操作系统的running) 进入 Timed Waiting 状态(阻塞),可通过state()方法查看其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException。睡眠结束后的线程未必会立刻得到执行。建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield (让出当前线程)
调用 yield 从JVM看它始终是在runnable状态。但是从操作系统看,是从Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程。具体的实现依赖于操作系统的任务调度器。
3.3 线程的优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
3.4 Join
用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。如在主线程中调用ti.join(),则是主线程等待t1线程结束
3.5 Interrupt
(1)方法区分
- interrupt(),打断线程【对象方法】。该方法实际上只是给线程设置一个中断标志,线程仍会继续运行,不涉及线程状态的改变。如果打断的正在运行的线程(runnable状态),则会设置打断标记(打断标记=true) ;park 的线程被打断,也会设置打断标记。与之相反,如果被打断线程正在sleep,wait,join (阻塞状态)会导致被打断的线程抛InterruptedException,并清除打断标记(打断标记=false)。简单来说一个处于阻塞状态的线程不存在由打断false-->true的状态变化;
- isInterrupted(),判断是否被打断【对象方法】,不会清除中断标记。Interrput()和isInterrupted()两个方法一般会一起使用。
- interrupted(),判断当前线程是否被打断【类方法】,会清除中断标记。举个例子,将一个正常线程interrupt(), 之后第一次调用是返回true。但是因为他会更改中断标记,如果再次调用则返回false。换一个角度,就是一个toggle切换的作用。
(2)不同状态下使用interrupt()
如果一个线程在在运行中被打断,打断标记会被置为true。正常运行的线程在被打断后,不会停止,会继续执行。如果要让线程在被打断后停下来,需要使用打断标记来判断。
如果是打断因sleep wait join方法而被阻塞的线程,会抛异常,将打断标记置为false。
(3)Interrupt 的应用-两阶段终止模式
当我们想要结束一个线程或者关闭jvm的时候,通过此模式可以优雅安全的关闭线程,让线程可以完成它本应完成的当前任务并可以附加一些收尾工作后再进行关闭
此模式下关闭线程会有一定延迟,主要在于被关闭线程需要执行完后,再进行关闭
假设有一个monitor的监控线程。其中线程里面有一部分是正常的代码,一部分是休眠的代码。然后改变这个线程中的打断标记,interrupt()方法。
正常状态,打断标记会改为true;
非正常状态,比如sleep()这种就为false。一般在sleep()状态被打断,会被抓住异常,进入catch 语句块,非阻塞状态,重写进行打断。Thread.currentThread().interrupt()
一旦打断标记为ture, 进入后事处理阶段。
(4)不推荐使用的打断方法
stop方法 停止线程运行(可能造成共享资源无法被释放),suspend(暂停线程)/resume(恢复线程)方法
3.6 守护线程
当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。只有守护线程例外,只要其他非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。比如垃圾回收器线程就是一种守护线程;Tomcat 中的 Acceptor 和 Poller 线程,Tomcat 接收到 shutdown 命令后,不会等等。
4. 线程状态
(1)操作系统层面
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联(例如线程调用了start方法)
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态。当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】;等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】;与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
(2)JVM层面
【NEW 】线程刚被创建,但是还没有调用 start() 方法
【RUNNABLE】 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
【BLOCKED】 ,【WAITING】 , 【TIMED_WAITING】 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。其中【BLOCKED】是和monitor 锁联系在一起(对象锁,内存); 而【WAITING】, 【TIMED_WAITING】则是和另外一个线程完成某个动作后唤醒。
【TERMINATED】 当线程代码运行结束
5. 统筹规划
通过线程的统筹规划来实现资源的最大化利用