什么是线程?
要说什么是线程,为了解释这个概念,我首先要从进程讲起。来看一下进程的概念:
进程是指可执行程序并存放在计算机存储器的一个指令序列,它是一个动态执行的过程。
看到这个概念后,是不是感觉完全懵了,那我们该怎么理解呢?我们来想象一下平时我们使用电脑的一个场景。
我们平时敲代码的时候是不是边听音乐边敲代码边用QQ跟女朋友聊天啊(反正我是除了最后一条,前两天都占了,呜呜)。音乐播放器、代码编辑器、QQ这三个软件是同时运行的,这样我们才能很多事情一起来完成,那么这三个软件可以同时运行,就是进程在起作用。我们可以打开Windows的任务管理器,Windows的任务管理器中是可以看到有进程这么一个选项卡。
打开windows任务管理器后,我们就能看到当前操作系统中所运行的所有进程了。比如说上图中我们看到的QQ的进程、Google浏览器的进程等等。而有的软件可以有多个进程,比如一些杀毒软件、数据库的软件等。
其实早期的操作系统都是单任务的操作系统,比如QQ,音乐播放器,它们只能单独运行,一个运行之后,才能下一个运行,比如大家想象一下,你先听歌曲,听完歌曲之后你才能在QQ中回复好友的问题,是不是感觉特别不方便啊。
而我们现在的操作系统都是多任务的操作系统,可以多个程序同时运行,我们可以边听歌边回复信息。 ,这就是我们的进程在起作用。
言归正传,我们现在说一下什么是线程:
线程是比进程还要小的运行单位,一个进程包含多个线程。
比如说一个程序是由很多行代码组成的,那么这些代码中就可以分成很多块放到不同的线程中,去分别执行,所以我们认为,线程相当于一个子程序。
现在知道进程和线程的概念之后,问题就来了,我们知道,程序的运行是靠CPU来处理的,那如果你只有一个CPU的情况下,怎么能保证这些程序都能同时运行呢?这里面我们可以想象成把CPU的执行时间分成很多的小块,每一小块的时间都是固定的,我们可以把这个小块叫时间片,时间片的时间可以非常短,比如说一毫秒,那么如果我们有音乐播放器、代码编辑器、QQ三个软件同时运行,那么它们三个如何去获取CPU的执行时间呢?这个其实是随机的,可以这样考虑,我们的音乐播放器运行一毫秒,然后它会把CPU的使用权转给代码编辑器,代码编辑器运行一毫秒将CPU的使用权转给QQ,那么这些程序就轮流的在很短的时间内使用CPU,对于CPU来讲,这些软件其实是轮流运行的,但是由于它运行的时间间隔非常的短,作为我们使用者来说,是感觉不到它的变化的,这样我们就会认为这些软件都是同时运行的,这就是为什么在只有一个CPU的情况下,这些软件能够同时运行的原因,这个叫做时间片的轮转。是通过对CPU的时间的轮转来达到同时运行的效果的。
线程的创建
线程创建有两种方式:
- 第一种:创建一个
Thread
类,或者一个Thread
子类的对象。 - 第二种:创建一个实现
Runnable
接口的类的对象。
这里面涉及到了一个Thread
类和一个Runnable
接口,我们来了解一下这两个系统为我们定义的类和接口都有哪些属性和方法:
Thread类
Thread
是一个线程类,位于java.lang
包下
Thread
类的常用方法
创建线程案例
- 通过继承Thread类的方式创建线程类,重写run()方法
在这我提醒一下,对于很多初学者来说总有一个疑问,为什么它继承
Thread
类就是一个线程了,这个我们说,Java
中很多东西都是人家写好了我们使用,Java
中就是这么为大家规定的,你只有通过继承Thread
类、实现Runnable
接口这两种方式得到线程,所以我们按照这种方式做就可以了,这样就能达到我们的目的。
代码演示
package com.thread;
class MyThread extends Thread{
@Override
public void run() {
System.out.println("该线程正在执行!");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();//启动线程
}
}
运行结果
不知道大家从上面代码中看出来什么没有,我来强调一下,启动线程的时候,不是调用
run()
方法,我们以前学习的时候,执行哪部分内容就调用相对应的方法,而在线程中,使用start()
方法去启动线程,启动线程、执行线程的时候执行的是run()
方法里面的代码,这个是需要我们注意的。
同时,还要注意一个线程只能执行一次,也就是只能调用一次
start()
方法
package com.thread;
class MyThread extends Thread{
@Override
public void run() {
System.out.println("该线程正在执行!");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();//启动线程
mt.start();
}
}
如图,如果多次调用
start()
方法,就会抛出异常。
创建多个线程案例
package com.thread;
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int i=0;i<=10;i++){
System.out.println(getName()+"正在运行");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt1 = new MyThread("线程一");
MyThread mt2 = new MyThread("线程二");
mt1.start();//启动线程一
mt2.start();//启动线程二
}
}
运行结果
在上面代码中我创建了两个线程,我们多运行几次,会发现每次的运行结果都是不一样的,从这里我们能体会到,线程要想去获得
CPU
的使用权,其实是随机的,其结果会出现很多种不同的情况。
Runnable接口
- 只有一个方法
run()
Runnable
是Java
中用以实现线程的接口- 任何实现线程功能的类都必须实现该接口
创建线程案例
为什么要实现Runnable
接口?
- Java不支持多继承
如果你的
Class
类已经继承了一个类,再去继承Thread
类是不可能的,所以这时候我们需要用接口去实现,因为接口可以同时实现多个接口
- 不打算重写
Thread
类的其他方法
我们知道,我们继承一个类,就会继承这个类中的所有方法,但对于线程来说,其实我们只需要重写
run()
方法就可以,如果不打算重写Thread
类中的其他方法,我们也可以用使用接口的方式。从我们的实际应用来看,使用实现Runnable
接口的方式应用更广泛一些。
代码演示
package com.thread;
class PrintRunnable implements Runnable{
@Override
public void run() {
int i=1;
while(i<=10){
System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
}
}
}
public class RunnableTest {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
Thread t1 = new Thread(pr1);
t1.start();
PrintRunnable pr2 = new PrintRunnable();
Thread t2 = new Thread(pr2);
t2.start();
}
}
运行结果
从上面的代码中,我们发现,用实现
Runnable
接口的形式去创建接口的时候,我们是三步走,先定义Runnable
实现类的对象,然后通过它创建线程类的对象,最后启动线程,所以我们启动线程只能通过Thread
类以及它的子类去启动。
多个线程处理同一个资源的情况
package com.thread;
class PrintRunnable implements Runnable{
int i=1;
@Override
public void run() {
while(i<=10){
System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
}
}
}
public class RunnableTest {
public static void main(String[] args) {
PrintRunnable pr = new PrintRunnable();
Thread t1 = new Thread(pr);
t1.start();
Thread t2 = new Thread(pr);
t2.start();
}
}
运行结果
出现上面这种运行结果,原因是
run()
方法被多个线程共享,多个线程也就是Thread
类的实例,也就是pr
对象被t1
和t2
两个Thread
类的实例共享,这适用于多个线程处理同一个资源的情。i
相当于一个资源,t1
和t2
共享了这个资源。
线程的状态
- 新建(New)
- 可运行(Runnable)
- 正在运行(Running)
- 阻塞(Blocked)
- 终止(Dead)
线程的状态首先是新建状态,创建一个
Thread
或者Thread
子类的对象的时候,线程就进入了新建状态。接下来是可运行状态,当已经创建好的线程对象调用start()
方法,这时候就进入了可运行状态,在前面我也说过,线程什么时候运行是由CPU
来决定的,只有当线程获取CPU
的使用权的时候,它才能执行。所以这里面,线程调用start()
方法之后,不是马上进入运行状态,而是进入可运行状态,这种状态也叫就绪状态,也就是我已经准备好了。接下来就是正在运行状态,一个处于可运行状态的线程,一旦获取了CPU
的使用权,就可以立即进入正在运行状态,接下来就是阻塞状态,当线程遇到一些干扰的时候,它将进入阻塞状态,也就是它不再执行,后面我在说生命周期的时候也会详细说如何进入阻塞状态,最后是终止状态,这就是线程的五个状态。
线程的生命周期
所谓的线程的生命周期,就是线程从创建到启动,直至运行结束的这段时间我们就叫它的生命周期。其实线程的生命周期就是我上面说到的五个状态相互的转换过程,可以通过调用Thread
类的一些相关方法来影响线程的状态,状态之间的转换就可以构成我们最终的生命周期了。
首先是新建状态,只要你创建了一个Thread
或者Thread
子类的对象,你就进入了新建状态,然后通过调用线程的start()
方法去启动线程,进入可运行状态,当处于可运行状态的线程获取CPU
使用权后,它将进入正在运行状态,如果一旦CPU
的使用权到时间了,那么线程就会从正在运行状态重新回到可运行状态,去等待获取下一次的CPU
使用权,所以这块从正在运行状态到可运行状态的一个条件就是时间片用完,还有一个就是调用yield()
这样一个方法,去从运行状态变成可运行状态。
那么再来看一下,正在运行状态到阻塞状态之间的转换,可以通过调用Thread
类中的一些方法来完成,比如说join()
方法、wait()
方法、sleep()
方法,这些都可以把线程从正在运行状态转换成阻塞状态,在后面我也会说到这些方法的使用。还有一种情况就是I/O
请求,I
是Input
,O
是Output
,就是输入输出请求的意思。阻塞状态我们可以看做一个正在运行的线程进入了一个暂停的状态,I/O
请求需要耗费一定的时间,这时候就可以让线程进入阻塞状态,等待I/O
请求完成,再继续进行执行。这是正在运行状态到阻塞状态的转换,反过来,阻塞状态的线程是不能直接转换成正在运行状态的,因为我之前说过要获取CPU
的使用权才能变成正在运行状态。
所以这里阻塞状态最终会转换成可运行状态,那么它要满足什么条件呢?这个条件和之前说的正在运行状态变成阻塞状态的条件是对应的,也就是解决这些问题的方法。第一个是等待调用join()
方法的线程执行完毕,第二个是调用notify()
方法或notifyAll()
方法,这两个方法其实是对应wait()
方法使用的,也就是调用wait()
方法的线程必须调用notify()
方法或notifyAll()
方法才能进入可运行状态,这个我会在后面说同步的时候详细说。再有就是之前调用sleep()
方法进行阻塞的线程,当你休眠超时以后,它就会重新变成可运行状态。最后一点就是I/O
请求完成,一旦I/O
请求完成,那么线程依然可以回到可运行状态。
最后,我们看到还有一个状态是终止状态,线程什么时候进入终止状态呢?对于一个新建的线程来说,如果它去调用stop()
方法,会进入终止状态,同样,对于一个可运行状态的线程和一个处于阻塞状态的线程,它们调用stop()
方法也会进入终止状态。在这我提一下,对于这个stop()
方法,其实已经不建议使用了,因为Java
的JDK
当中会显示版本过期,不推荐使用了。那么一个处在正在运行状态的线程如何进入终止状态呢?这里有几种情况,第一种当然也是调用stop()
方法,再有就是一个线程执行完毕了以后或者是一个正在执行的线程因为某些原因异常终止了,整个程序都终止了,它也会进入终止状态,就跟一个人的生命一样,从出生,然后长大,最后结束这样的一个过程。
结束
讲到这,关于线程的知识点也讲清楚了,从线程的创建到使用,我都用通俗易懂的话语做了解释,希望能对读者朋友们有所