1.进程
是操作系统为应用程序分配资源的基本单位,比如操作系统会为一个应用程序分配独立的工作空间,硬件资源,任务调度等。一个应用程序就是一个进程。
2.线程
是cpu执行的基本单位,可以理解为一个基本的执行流,一个进程中至少有一个线程。进程是线程的集合体或者载体。
3.线程模型
线程分为内核线程和用户线程。
内核线程就是操作系统自己实现的一套线程机制,实现一套线程机制并不是像java一样创建一个Thread对象即可,而是除了创建线程对象,还要基于操作系统及硬件的不同(比如cpu是多核单核等),制定何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等事宜,而内核实现了这一套机制。
用户线程就是用户程序中创建的线程,和内核线程一样,线程是交给处理器执行,但是具体的执行和分配机制需要用户程序自己实现,就是说也要根据机器的不同实现一套线程运行机制才行。但是实现这样一套机制是极其复杂的,而且还要考虑不同的机器平台,所以说有一定的困难度。
那我们开发的应用程序线程是怎么实现的呢,一般的应用程序实现的线程根据其特点可以分为三种:
- 用户线程和内核一一对应的1:1模型
- 用户线程和进程多对一的1:N模型
- 用户线程和内核线程多对多的M:N模型
(1) 1:1模型
应用程序创建一个线程实际上是通过内核提供的内核线程api调用与一个内核线程建立一比一映射关系,也就是说看起来应用程序创建了用户线程,但其实应用程序只是创建线程对象,通过线程对象调用了内核api创建了内核线程,对于应用程序来说就比较省事了,因为具体的线程机制都是由内核完成。
这种模型免不了用户态到内核态的切换,性能方面会被限制。
(2) 1:N模型
就是上面所说的,由用户程序自己实现线程机制,包括线程创建,线程调度等。
这种模型基本能避免用户态到内核态的切换,这就可以支持更多的线程并发,不好的地方就是实现困难。
(3) M:N模型
就是用户程序中的用户线程和内核线程不是一比一映射,而是多条用户线程对应一条内核线程或者多条内核线程。
这种模型既能够保证并发量,提高性能,又能利用内核的调度机制,但是免不了自己实现一套线程机制,依然具备很高的复杂性。
java在实现线程机制这条路上可以说对于三种模型都曾有过实现。其实采用哪种线程模型依赖于所使用的操作系统是否支持,就拿Hotspot这款虚拟机来说,它在不同操作系统上的实现就不一样,在Solaris平台的HotSpot虚拟机,由于操作系统的线程特性本来就可以同时支持 1:1及N:M
但是主流的java虚拟机hotspot在主流的操作系统win和linux,采用的都是1:1模型。即线程调度交个内核来完成,这样我们在创建线程的时候就受到了资源的很大限制,比如我们无法做到创建大批量的线程出来。
举个例子:
内核是一个国有工厂负责加工产品,cpu是加工产品的机器,一个用户程序就是一个私有企业,也就是一个进程,而线程就是私企和国企里面的工作人员。每个私企员工都会将一定的原材料送到国企进行加工。
1:1模型中,每当一个私企员工携带原材料来加工,都会有一个国企的员工接待,负责加工流程中的所有事情,直到加工完成并将私企员工送走。因为有很多私企,一个私企里面有很多私企员工,而国企员工相对较少,这样一对一的服务,如果私企的人来的太多,会把国企人员累死,整个生产线会瘫痪。
1:N模型中就是每个私企在国企里面租一台机器的使用时间,就是某个时间段,由某个私企承包,这个时间段内机器只为这个私企工作,这种情况下只要在这个时间段内,私企想派多少人去加工就派多少人去加工。
M:N模型中,某个私企大概有100人携带原材料加工,私企感觉这样效率有点低,就想了个办法,和国企达成某种协议,让国企出几个人专门服务于私企的这100个人,这样效率会提升了。
4.线程调度
多线程情况下,能够让每个线程都能有条不紊的得到运行就是线程调度。
我们知道cpu处理器是单线流水运行,线程是运行在cpu上的,一个时间点上只会有一条线程正在被cpu运行。
基于以上情况,线程要如何调度呢,一般情况下,操作系统有两种调度方式:
(1) 协同式调度
正在执行的线程自己控制自身的执行时间,并且当前线程执行完后由自身告知操作系统可以调度到其他线程上了。
这种方式相对来说实现简单,不用考虑同步的问题。
但是缺点也很明显,这种调度方式中有两个点要注意,线程多长时间可以执行完?线程一定能通知操作系统调度其他线程吗?这两点无法保证的话就会引发一些问题,比如当前线程本身运行时间很长,执行逻辑中有一些io等待操作,这就造成cpu是空闲的,资源大大浪费;再比如如果代码或者业务逻辑出了问题稍有不慎就会造成无法通知操作系统调度其他线程,这个是灾难性的。
(2) 抢占式调度
由操作系统为每个线程分配操作时间,并且由操作系统自主负责调度其他线程。
cpu处理整个过程被分成若干个极小的时间片段,每个线程可以被分配多个时间片段。然后操作系统会按照分配情况进行调度。
这种方式可以让所有的线程看起来是同时运行的,即便有一条线程出了问题,也不会影响其他线程。
主流的平台linux的内核线程都是抢占式调度,这种方式的缺点就是接下来要说的上下文切换。
4.线程上下文切换
当处理器要运行线程时,除了运行代码外还要有上下文数据的支撑。而这里说的“上下文”,以程序员的角度来看,是方法调用过程中的各种局部的变量与资源;以线程的角度来看,是方法的调用栈中存储的各类信息;而以操作系统和硬件的角度来看,则是存储在内存、缓存和寄存器中的一个个具体数值。
物理硬件的各种存储设备和寄存器是被操作系统内所有线程共享的资源,比如线程A正在运行,线程B正在挂起等待运行,此时线程A的时间片段运行完毕了,线程B就会被调度。从线程A切换到线程B去执行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,这样线程B被重新激活后才能仿佛从来没有被挂起过。这种保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中的来回拷贝,这便是上下文切换,同时这个操作不是一个轻量级的操作,你想每个时间片段是很短的,也就意味着短时间内要做大量的上下文切换,这就一定会造成很大的时间消耗。
说到这里,java开发中的线程是java线程和内核线程一比一映射的1:1线程模型,一般java运行在linux系统,所以java开发中常遇到的性能影响点是”用户态转换内核态和上下文切换“。
5.java线程
java中的线程类是Thread,它定义了java层面线程创建,线程操作的一些列方法。
java中线程实现有三种方式:
//1.继承thread
Student extends Thread
Student xiaoming = new Student("小明",punishment);
xiaoming.start();
//2.实现Runnable
Student implements Runnable
Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
xiaoming.start();
//3.任务FutureTask实现
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception{
return "hello";
}
}
public static void main(String[] args) {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask ());
new Thread(futureTask).start();
try{
String result=futureTask.get();
}catch(){
e.printStackTrace();
}
}
- 第一种方式继承Thread类,所以子类也是一个Thread类,这样子类是一个携带任务的线程类,任务和线程牢牢绑在一起不可分割,且子类继承Thread类,就再也不能继承其他类。
- 第二种方式,Runnable是任务类,子类实现Runnable后依然是一个任务类,而且还能实现其他的类,这种方式中任务和线程隔离,一个任务可以被多个线程执行。符合java职责分离的设计原则和面向接口编程的原则。
以上两种方式均没有返回值,而第三种方式可以支持返回值,后面我们细讲FutureTask。
我们来解析一下上面的前两种方式:
Thread是java中的线程实体类,Runable是任务实体类,他们两个的关系为,Runable代表任务,Thread代表任务的载体,以上面的例子来说,Thread就是企业内的工作人员,而Runable是具体的加工任务。
public interface Runnable {
public abstract void run();
}
Runnable是一个接口,run是任务方法,自定一个类并实现Runnable类,然后重写run方法定义具体的任务逻辑,这个类就是一个具体的任务类。
任务类自身不能运行,需要依赖载体。
public class Thread implements Runnable {
private Runnable target;
public Thread() {
}
public Thread(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
}
Thread线程作为任务的载体,通过实现Runnable而具备携带任务的能力,通过源码可知,这个任务逻辑可以来自于自身,也可以来自外界传进来的任务实体Runnable。
Thread线程除了可以携带任务,更重要的是能够驱动任务运行,start方法就是启动开关,其内部是本地方法,由jvm内部实现,不难想象这个方法的底层一定是调用了内核线程api,与内核线程做映射,然后jvm回调java中的run方法。
6.java线程状态
java中的线程具有六种状态。
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
- 等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线唤醒。一般在调用wait方法和sleep方法的时候处于这种状态。
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
7.java 线程 API
睡眠
public static native void sleep(long millis);
获取当前线程对象
public static native Thread currentThread();
让出cpu
public static native void yield();
设置优先级
public final void setPriority(int newPriority);
阻塞等待执行完成
public final void join();
设置为守护线程
public final void setDaemon(boolean on);
让可中断方法中断
public void interrupt();
获取中断位状态
public static boolean interrupted();
获取中断位状态,并重置
public boolean isInterrupted();
这些方法有一些不是很常用,所以了解即可,重点说一下join,interrupt,interrupted,isInterrupted。
join方法是一个成员方法,调用某个线程的join方法,会让主线程一直处于wait状态(join方法底层源码中通过不断调用wait方法实现),等待这个线程处理完成再继续执行下去。
interrupt是一个成员方法,这个方法会中断静止线程,而运行中的线程只能被设置为可中断标志位为true,怎么理解呢?就是说如果线程处于sleep、wait状态,这个时候调用interrupt会把这个阻塞状态唤醒并抛出异常,之后该线程继续运行,如果线程正在运行中(非Waiting状态),此时调用interrupt只会给线程的可中断状态设置为true,对线程运行不会有任何影响。
调用某个线程的interrupt方法,给线程设置可中断状态的意义是什么呢?因为线程运行过程中可能正在处理数据,这个时候如果人为中断线程是不安全的,所以要等到线程处于一个安全位置中断才比较合理,那什么时候才是安全的位置呢,这个程序员自己决定,当处于安全位置的时候给一个可中断标示,这样中断的时候判断一下可中断状态再决定是否中断即可。juc源码中随处可见该方法的应用。
上面说了中断的时候判断一下可中断的状态,interrupted这个方法就是获取线程的中断状态,而isInterrupted方法也是获取线程的可中断状态,不同点在于isInterrupted方法在获取中断状态后,会顺便把中断状态重置为false。