1.进程与线程
进程与线程的由来:
计算机cpu工作原理,操作系统调度单核cpu时其实是在不停的切换任务,因为cpu同一时间只能做一件事,而且cpu做事速度很快,往往在完成一件任务时需要等待其他零件完成任务(比如硬盘读写i/o阻塞),此时为了最大限度的理由资源,cpu只能在不同任务间来回切换,就是我们所说的并发了。
但并发带来了一些问题,原先单任务现在变成了多个任务,任务之间的切换怎么进行,这时候就引入了进程的概念。
用进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。
当进程暂时时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。这就是并发,能够让操作系统从宏观上看起来同一个时间段有多个任务在执行。
换句话说,进程让操作系统的并发成为了可能。
而线程是进程的子集,线程也是一个应用程序最小的执行单元。
进程的出现解决了操作系统的并发问题,但是对于一个进程来说,它内部也需要执行多个子任务,比如读取,计算,实时显示,为了更大限度的提升效率,人们引入了线程的概念。进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。
注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
2.进程与线程的区别
-
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
-
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
-
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
-
调度和切换:线程上下文切换比进程上下文切换要快得多
生活中的举例:你在上网时,挂着QQ,看着爱奇艺,并用迅雷下载着文件。这时候QQ,爱奇艺,迅雷就可以认为是一个个进程。而你在QQ中可以和张三、李四等人同时聊天,那么张三、李四等人的聊天界面就是一个个线程。
问题:是否多线程的性能一定就由于单线程呢?
不一定,要看具体的任务以及计算机的配置。比如说:对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,
因为解压文件需要一直占用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。但是对于比如交互类型的任务,肯定是需要使用多线程的、而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。
虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。
因此,在实际编程过程中,要根据实际情况具体选择。
3.java中的线程创建方法
前文我们已经对线程的概念有了一个初步的了解,那么怎么才能在JVM中创建一个线程呢 ?
在java中创建线程一般有两种方式:1)继承Thread类 2)实现Runnable接口
-
1.通过继承Thread类来实现创建线程
/** * 1 .继承Thread类 * 继承Thread类的话,必须重写run方法,在run方法中定义需要执行的任务。 **/ class ThreadOne extends Thread{ @Override public void run(){ System.out.println(Thread.currentThread().getId()+"开始运行 了"); } } /** * 创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线 程。 **/ public static void main(String[] args) throws Exception{ new ThreadOne().start(); //jdk1.8下用 lambda表达式简化写法 new Thread(()-> System.out.println(Thread.currentThread().getId()+"开始运行 了")).start(); }
创建好线程类后,此时线程只是一个不同的java对象,还不能称之为线程,只有通过调用start()方法才能启动该线程。 注意:不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行ThreadOne对象中的run()方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
- 通过实行Runnable接口来创建线程
Java为我们定义了一个Runnable接口,我们可以通过实现该接口并重写接口中的run()方法来实现线程的创建
//自定义一个Runnable接口的实现类,并重写其中run方法
class ThreadTwo implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getId()+"开始运行了");
}
}
public static void main(String[] args) throws Exception{
//创建Thread类并将接口实现做入参,并调用start方法
new Thread(new ThreadTwo()).start();
}
查看jdk源码我们可以发现,Runnable接口结构很简单,只是定义了一个无参、无返回值的run方法。
所以其实严格意义上说通过实行Runnable接口来创建线程是一种不严谨的书法,在jdk中代表线程的只有Thread这一个类,线程的执行单元就是run方法,查看Thread.run源码
我们可以发现,run方法只是执行了target中的run方法,而target是什么呢。查看Thread的构造函数我们可以发现:
这里的target就是我们传递进来的的Runnable接口的实现,Runnable的实例仅仅为线程提供了他的执行单元逻辑。
所以说创建线程有两种方式,第一种是创建一个Thread的实现类,第二种是实现Runnable接口,这种说法并不严谨。
严谨点的说法应该是:创建线程只有一种方式就是构造Thread类,而实现线程的执行单元则有两种方式:第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并将Runnable接口的实例作为入参并构造Thread实例。
4.线程生命周期详解
线程作为程序执行的最小单位,他也有着一个完整的生命周期:
如上图,线程的生命周期大体分为如下5个部分:
NEW(新建) , RUNNABLE(可运行) , RUNING (运行), BLOCKED(阻塞) , TERMINATED(消亡)
下面对每个状态进行具体讲解
-
NEW 状态
当我们创建一个线程对象,如 new Thread() 后,此时线程还没开始调用start方法启动,那么这时候的线程就处于NEW 状态。其实跟确切的说,此时线程还不存在,在没调用start() 方法前,你只是用关键字 new 创建了一个普通的Java对象。
NEW 状态可以通过start() 进入 RUNNABLE状态。
-
RUNNABLE 状态
线程对象创建后要想变成RUNNABLE 状态必须调用 start()方法,调用该方法后,才能算是真正的在 JVM进程中创建了一个线程。但线程一旦启动并不会马上就能得到执行,线程的运行与否与进程一样是取决于CPU的调度的,我们就把这种等待执行的状态称之 RUNNABLE(可运行)状态,表示它具备了执行资格,但是还没能真正的执行起来。
由于此时线程并不是Running状态,所以该状态下的线程不会直接进入 BLOCKED 和 TERMINATED状态,即使在线程的执行中调用了wait、sleep或者其他block的IO操作等,也必须是线程获得了CPU的调度执行权才可以转变状态。
RUNNABLE的线程只能意外终止 或者进入RUNNING状态。
-
RUNNING状态
当CPU通过轮询或者其他方式从任务可执行队列中选中了线程,此时它才能真正的执行自己的内部逻辑代码,而此时线程的状态就是RUNING状态。可以说 一个正在RUNNING 状态的线程事实上也是RUNNABLE 的,但是反过来说则不成立。
RUNNING状态下的线程状态常见转换:
-
RUNNING 变成 BLOCK 状态
1.调用了sleep、wait方法而进入了waitSet中。
2.进行某个阻塞的IO操作,比如网络数据的读写
3.为获取某个锁资源,从而加入到该锁的阻塞队列中
-
RUNNING 变成 RUNNABLE 状态
1.CPU调度器轮询使该线程放弃了执行
2.线程主动调用了yield方法,放弃了CPU的执行权
-
RUNNING 变成 TERMINATED 状态
1.调用stop方法
-
-
BLOCKED 状态
线程由于某些原因进入了阻塞状态,详见上文,线程在BLOCKED状态可进行的状态切换:
-
进入 RUNNABLE状态
1.线程完成了指定时间的休眠
2.线程阻塞的操作结束,比如读取到了想获取的数据
3.Wait中的线程被其他线程 notify/notifyall唤醒
4.线程获取到了某个锁资源
5.线程在阻塞过程中被打断,比如其他线程调用了interrupt方法
-
进入 TERMINATED 状态
调用stop或者意外死亡(JVM Crash)
-
-
TERMINATED状态
线程的最终状态,该状态的线程不会在进行状态变换,意味着整个生命周期都结束了。线程进入到TERMINATED状态的情况:
1.线程运行正常结束,结束生命周期
2.线程运行出错意外结束
3.JVM Crash,导致所有线程都结束
了解线程的生命周期各状态之间的转换非常重要,每种语言具体定义的状态枚举也许不同,但总体来说都会在这5种状态的范畴之内。