在日常工作中,除了线程,还有进程、协程等,现在来看看这些基本概念。
什么是进程,相信大家都知道什么是进程却很难解释清楚。百科中的解释是:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 实际上,可以理解为,进程是操作系统中的某个程序关于某个数据集合的一次运行活动。是操作系统动态执行的基本单元。操作系统以进程为基本单元进行资源分配和任务执行。
正如任务管理器中所看到的,所有程序都是通过进程来运行的。进程是计算机分配资源和执行任务的基本单元。只有进程启动了,程序才能正常运行,否则,程序就是静态的文件,不会工作。是进程将程序有静态文件变成了动态的执行过程。 进程在执行的过程中,互相之间的堆和栈的内存空间都是独立的,不能共享。只能通过共享存储、信号量、消息队列等相关方式进行通信。 在linux系统中,所有进程信息都采用task_struct结构体进行描述。每个进程都有一个pid编号,另外我们需要知道的是linux进程的状态。
线程才是本文需要讨论的重点。最开始的操作系统,只有进程的概念,并没有线程。但是随着计算机的发展,对CPU的要求越来越高,多进程模式进行同步的开销非常大,进程间切换是很消耗资源的。因此,就发明了线程,在进程内部,在抽象得更加细化的线程概念,一个进程可以有多个线程。这样一来,同一个进程的线程之间,除了栈是私有的之外,堆区的内存就能共享。这样进程就由其内存空间和一个或者多个线程组成。 实际上写过代码的人都知道,线程这个概念是一个功能抽象的过程。之前只能用一个进程来执行,现在分为多个线程之后,由于CPU时间片的关系,可以认为多个线程能同时工作。 我们来对比一下进程和线程:
进程与线程的关系如下:
多线程抽象之后的时间片分配:
实际上通过时间片之后,由于时间片很短,因此用户在使用上就感觉多线程能同时工作。 线程的生命周期我们将在后面单独来说明。
协程,又称微线程,纤程。英文名Coroutine。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。协程实际上是在线程概念上再次进行的抽象。我们回顾线程的概念,通过线程解决了进程切换资源开销的问题。这样很多功能就可以通过在一个进程中由多个线程来实现。但是随着计算机系统的发展,计算机的性能虽然越来越好,但是赶不上需求发展的速度,虽然多线程也能解决高并发问题,但是不可忽视的是线程是一个操作系统底层的概念。一方面,线程切换需要开销,另外一方面,线程是一个底层概念,实现线程同步的话需要非常复杂的代码,来实现不同的锁机制,确保线程安全等问题。因此有没有一种不需要关心线程的切换,只需要固定的线程数,而是将任务拆分,类似于CPU时间片一样,如果我们可以将我们设计的程序,也可以类似于CPU时间片那样拆分为一个个细小的单元,之后我们就不用关心线程的切换,只需要将这些通用的任务进行切换即可。这样就会更加简单和提升效率。协程就应运而生。 我们再回想一下,如果再多线程的情况下编程,任务是不可拆分的,那么势必造成某些线程长期占有CPU执行,而某些线程由于执行的速度特别快,执行完成从之后就会在线程池中处于空闲状态。由于操作系统底层实际上只知道线程的运行状态,有多个线程,那么就必须维护多个。如果有线程偷懒的话,实际上是不经济的。这就好比在一个工厂中,由于任务拆分得不细,那么只有那么几个工人是忙碌状态。我们再来看看现代工业的流水线,任务分解到一个个的动作,在流水线上操作。这样工人的时间都能尽量用起来。对应到线程也是一样。只要线程存在,就会分配时间片,入果这个线程没有工作,那么就会资源浪费。现在我们可以将任务拆分得更细,这样一来,线程就不会空闲,CPU的利用率就上升了。 这样一来,首先会大大提升执行效率,不会有线程的切换开销。其次,就是不再需要考虑锁机制,在用户层面,只需要考虑如何将任务分解即可。
在jvm中,jvm中的线程实际上是和操作系统的线程一一对应的。我们每当new一个thread,实际上就是在操作系统中真正的创建一个Thread。本文暂不涉及linux底层的线程模型。实际上线程在linux中就是以轻量进程的形式来运行的。线程的生命周期基本上等同于进程的生命周期。
在传统的线程模型中,可分为三态或者五态模型。 如下即是一个五态的模型:
这是传统的线程五态模型,在不同的语言中,可能会被简化或者细化。java就对部分状态进行了简化和细化。我们来看看JAVA中的线程模型。
在java中,线程的生命周期分为:
实际上BLOCK、WAITING、TIMED_WAITING三种状态都是前面五态模型中的阻塞状态的细化。这三种状态的任意一种状态都不能获得CPU的执行权。而RUNNABLE则是将五态模型中的就绪和运行状态进行了合并。对应起来如下:
实际上这个图可以更细:
这就是java线程中各状态转换的情况。
java刚创建的线程是NEW状态,在java中,实现线程的方法有两种,一种是继承Thread类,一种是实现Runnable接口。
代码如下
class MyThread extend Thread{
public void run(){
//详细内容
}
}
MyThread t = new MyThread();
另外一种方法是实现Runnable接口。
class MyThread implements Runnable {
@Override
public void run() {
//详细内容
}
}
Thread t = new Thread(new MyThread());
以上就是在java中启动线程的两种方法。 之后只要执行start方法,就完成了从NEW到RUNNABLE状态的转换。
MyThread t = new MyThread();
t.start();
在RUNNABLE到WAITING状态装转换的过程中,有如下情况:
从RUNNABLE到TIME_WAITING的转换主要有如下情况:
从RUNNIG状态到BLOCKED的过程只有一种情况,那就是使用synchronized的时候,synchronized修饰的方法或者同步块,同时只允许一个线程执行,其他线程就会等待,这种情况下就会从RUNNBING转换到BLOCKED状态。当等待的线程获得了锁之后,就会从BLOCKED状态转变为RUNNING状态。
线程执行完成之后,会自动转换到TERMINATED状态。另外在run的过程中如果遇到异常,也会停止,线程状态会变为TERMINATED状态。如果run的过程很慢需要终止,我们需要使用interrupt()方法。 需要注意的是 stop()方法已经标注为@Deprecated,不再建议使用。 关于stop方法和interrupt方法的区别,我们在后续讨论Thread源码的时候详细来分析。
本文重点是对线程的定义及其生命周期进行了分析,同时也了解了进程和协程。需要注意的是五态模型,这实际上是一个抽象的概念。实际上在linux底层实现线程的时候,细节上会有很多的不同。我们需要掌握java线程的状态模型以及各状态之间的转换。