本文转载自微信公众号《太斗闲若如》,作者太斗闲若如。转载本文请联系仙若如大师公众号。什么是线程?什么是“线程”,为了解释这个概念,我先从“进程”说起。我们来看看“进程”的概念:?进程是指可执行程序存储在计算机内存中的一系列指令。这是一个动态的执行过程。?看到这个概念是不是感觉完全一头雾水,那我们应该怎么理解呢?想象一下我们平时使用电脑的场景。我们平时敲代码的时候,是不是用QQ和闺蜜聊天,边听音乐边敲代码(反正除了最后一个,前两天占满了,呜呜呜)。音乐播放器、代码编辑器和QQ是同时运行的三个软件,这样我们可以一起完成很多事情。那么这三个软件就可以同时运行了,也就是“进程”在工作。我们可以打开Windows的任务管理器,在Windows的任务管理器中,我们可以看到有一个“进程”的标签。打开windows任务管理器后,我们可以看到当前操作系统运行的所有进程。比如上图中我们看到的QQ的进程,谷歌浏览器的进程等。还有一些软件可以有多个进程,比如一些杀毒软件,数据库软件等。其实早期的操作系统都是单任务操作系统,比如QQ、音乐播放器。他们只能独立运行。QQ回复朋友的问题是不是特别不方便?而我们现在的操作系统是多任务操作系统,可以同时运行多个程序,我们可以边听歌边回复信息,这是我们工作的过程。言归正传,说说什么是线程:?线程是比进程更小的一个运行单元,一个进程包含多个线程。?比如一个程序是由很多行代码组成的,那么这些代码可以分成很多块放在不同的线程中分别执行,所以我们认为“一个线程相当于一个子程序”。既然知道了“进程”和“线程”的概念,问题就来了。我们知道,程序的运行是由“CPU”来处理的。如果你只有一个“CPU”,你怎么能保证这些程序呢?它们可以同时运行吗?这里我们可以想象一下,“CPU”的执行时间被分成了很多小块,每个小块的时间是固定的。我们可以把这个小块叫做“时间片”,时间片的时间可以很短,比如一毫秒。如果我们同时运行音乐播放器、代码编辑器、QQ软件,它们的CPU执行时间如何获取呢?这个其实是随机的,可以这样想,我们的音乐播放器运行一毫秒,然后把CPU使用权转移给代码编辑器,代码编辑器运行一毫秒,把CPU使用权转移给QQ,然后这些程序会在很短的时间内轮流执行。CPU在一定时间内被使用。对于CPU来说,这些软件其实是轮流运行的,但是由于它运行的时间间隔很短,作为我们的用户,我们感受不到它的变化,所以我们会认为这些软件都是同时运行的,这就是为什么这些软件可以在只有一个CPU的情况下同时运行的原因。这称为时间片旋转。同时运行的效果是通过轮换CPU时间来实现的。线程创建创建线程有两种方式:第一种:创建一个Thread类,或者Thread子类的一个对象。第二种:创建一个实现了Runnable接口的类的对象。这涉及到一个Thread类和一个Runnable接口。下面看看这两个系统为我们定义的类和接口的属性和方法:通过继承Thread类来创建线程类,并重写run()方法?这里要提醒很多初学者,总是有一个疑问,为什么继承Thread类是线程,我们在里面说了很多东西Java是别人写的,我们用。Java就是这样规定给大家的。只能通过继承Thread类并实现Runnable接口来获取线程,所以我们采用这种方式。就是这样,所以我们可以实现我们的目标。?代码演示包com.thread;classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("线程正在执行!");}}publicclassThreadTest{publicstaticvoidmain(String[]args){MyThreadmt=newMyThread();mt.start();//启动线程}}运行结果?不知道大家从上面的代码中看出了什么。强调一下,在启动线程的时候,不是调用run()方法。之前我们学习的时候,执行到哪一部分的内容,就会调用对应的方法,在线程中,使用start()方法来启动线程。当启动线程并执行线程时,会执行run()方法中的代码。这需要我们注意。??同时还要注意一个线程只能执行一次,即start()方法只能调用一次?packagecom.thread;classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("线程正在执行!");}}publicclassThreadTest{publicstaticvoidmain(String[]args){MyThreadmt=newMyThread();mt.start();//启动线程mt.start();}}?As如图所示,如果多次调用start()方法,会抛出异常。?创建多线程案例packagecom.thread;classMyThreadextendsThread{publicMyThread(Stringname){super(name);}@Overridepublicvoidrun(){for(inti=0;i<=10;i++){System.out.println(getName()+"Running");}}}publicclassThreadTest{publicstaticvoidmain(String[]args){MyThreadmt1=newMyThread("线程1");MyThreadmt2=newMyThread("线程2");mt1.start();//启动线程1mt2.start();//启动线程2}}运行结果?在上面的代码中,我创建了两个线程,我们运行几次,我们会发现每次运行的结果都不一样,从这里我们可以实现也就是说,如果一个线程想要获得CPU的使用权,其实是随机的,结果会有很多种不同的情况。?Runnable接口只有一个方法run()。Runnable是Java中用来实现线程的接口。任何实现了线程功能的类都必须实现这个接口才能创建线程。为什么要实现Runnable接口?Java不支持多重继承?如果你的Class类继承了一个类,那Thread类是不可能继承的,所以这时候我们就需要使用接口来实现,因为接口可以同时实现多个接口?不打算重写Thread类的其他方法?我们知道,我们继承一个类会继承这个类中的所有方法,但是对于线程,我们只需要重写run()方法即可。如果我们不打算重写Thread类中的其他方法,我们也可以使用interface的方式。从我们的实际应用来看,Runnable接口的实现方式应用更为广泛。?代码演示包com.thread;classPrintRunnableimplementsRunnable{@Overridepublicvoidrun(){inti=1;while(i<=10){System.out.println(Thread.currentThread().getName()+"Running"+(i++));}}}publicclassRunnableTest{publicstaticvoidmain(String[]args){PrintRunnablepr1=newPrintRunnable();Threadt1=newThread(pr1);t1.start();PrintRunnablepr2=newPrintRunnable();Threadt2=newThread(pr2);t2.start();}}运行结果?从上面的代码中我们发现,在以实现Runnable接口的形式创建接口时,要分三步走,首先定义Runnable实现类的对象,然后通过它创建一个线程类对象,最后启动线程,所以我们只能通过Thread类及其子类来启动线程。?多线程处理同一个资源包com.thread;classPrintRunnableimplementsRunnable{inti=1;@Overridepublicvoidrun(){while(i<=10){System.out.println(Thread.currentThread().getName()+"Running"+(i++));}}}publicclassRunnableTest{publicstaticvoidmain(String[]args){PrintRunnablepr=newPrintRunnable();Threadt1=newThread(pr);t1.start();Threadt2=newThread(pr);t2。start();}}运行结果?出现上面的运行结果是因为run()方法被多个线程共享,而多个线程都是Thread类的实例,即pr对象被两个Thread类t1和t1共享t2实例共享,适用于多个线程处理同一个资源。i相当于一个资源,t1和t2共享这个资源。?线程状态新建(New)可运行(Runnable)运行(Running)阻塞(Blocked)终止(Dead)?线程的状态首先是“新建状态”。当创建Thread或Thread子类的对象时,线程就进入了“新建状态”。接下来是“可运行状态”。当创建的线程对象调用start()方法时,就进入了“可运行状态”。前面说过,线程何时运行是由CPU决定的,只有当线程获得CPU的使用权后才能执行。所以这里,线程调用start()方法后,并没有立即进入“运行状态”,而是进入了“可运行状态”。这种状态也叫“准备状态”,意思是我准备好了。接下来就是“运行态”,一个处于“可运行态”的线程,一旦获得CPU的使用权,就可以立即进入“运行态”,接下来就是“阻塞态”,当线程遇到一些干扰时,就会进入“阻塞状态”,即不再执行。后面讲“生命周期”的时候也会详细讲解“如何进入阻塞状态”,最后是“终止状态”,也就是五种状态的线程。?线程生命周期所谓线程的“生命周期”,就是一个线程从“创建”到“开始”再到“运行结束”的这段时间,我们称之为它的“生命周期”。其实线程的生命周期就是我上面提到的五种状态之间的转换过程。你可以通过调用Thread类的一些相关方法来影响线程的状态。“状态之间的转换”就可以构成我们最终的“生命周期”。首先是“新状态”。只要你创建了Thread或者Thread子类的对象,就进入了“新状态”,然后通过调用线程的start()方法进入“可运行态”,处于“可运行态”的线程获得CPU使用权后,会进入“运行态”,等待下一次CPU使用对,所以这个block从“runningstate”到“runnablestate”的条件之一就是“时间片”用完,还有一个就是调用yield()。从“运行状态”变为“可运行状态”。那我们再来看一下,“运行态”和“阻塞态”的转换可以通过调用Thread类中的一些方法来完成,比如join()方法,wait()方法,sleep()方法,这两者可以将线程从“运行态”转换为“阻塞态”,这些方法的使用后面也会讲到。另一种情况是I/O请求,I是Input,O是Output,表示输入输出请求。“阻塞状态”可以看作是一个正在运行的线程进入了挂起状态。I/O请求需要一定的时间。这时线程可以进入“阻塞状态”,等待I/O请求的完成。继续执行。这是从“运行状态”到“阻塞状态”的转变。反之,处于“阻塞态”的线程是不能直接转为“运行态”的,因为我之前说过,它需要获得CPU的使用权才能成为“运行态”。那么这里的“blockedstate”最终会转化为“runnablestate”,那么它要满足什么条件呢?这种情况对应于前面所说的“运行状态”变为“阻塞状态”的情况,即解决这些问题的途径。第一个是等待调用join()方法的线程完成,第二个是调用notify()方法或notifyAll()方法。这两个方法实际上是用于wait()方法的,即调用wait()方法的线程必须调用notify()方法或notifyAll()方法才能进入“可运行状态”。这个我后面讲同步的时候再详细讲。然后就是之前调用sleep()方法阻塞的线程。当您“睡眠超时”时,它将再次变为“可运行”。最后一点就是I/O请求完成了。一旦I/O请求完成,线程仍然可以回到“可运行状态”。最后,我们看到还有一个状态就是“终止状态”。线程什么时候进入“终止状态”?对于一个新创建的线程,如果调用了stop()方法,就会进入“终止状态”。同样,对于处于“可运行状态”的线程和处于“阻塞状态”的线程,调用stop()方法也会进入“终止状态”。这里提一下,其实不推荐使用stop()方法,因为JavaJDK会显示“版本过期”,所以不推荐。那么处于“运行状态”的线程是如何进入“终止状态”的呢?这里有几种情况。第一种当然是调用stop()方法,另一种是在一个线程执行完或者一个线程正在执行之后。线程因为某种原因异常终止,整个程序终止,也会进入“终止状态”,就像人的生命一样,从出生,然后长大,最后结束这样一个过程。到此为止,关于线程的知识点我也解释清楚了。从线程的创建到使用,到线程的状态到生命周期,我都用通俗易懂的语言进行了讲解。希望读者朋友们能有所帮助。
