前言
我们在工作中线程技术很多情况下都能用的到,而且我们在面试的时候,线程技术基本上也是必问的。今天我来从线程的实现方式以及线程的生命周期做一个全面的讲解与分析,帮助大家能更好的去了解线程技术。
概念
我们先来了解下线程和进程的概念以及区别:
1、什么是进程
进程是计算机执行应用程序的执行动作。当你运行一个程序的时候也就是完成了一个进程的启动(比如启动微信或者QQ)。如图所示
2、什么是线程
线程是进程内独立运行的一个单位,而进程他可以由一个或者多个线程组成,但是一个线程只能对应一个进程。
如何创建线程
创建线程有继承Thread类、实现Runnable接口或者通过Callable和Future创建线程三种方法,具体创建过程如下:
1、继承Thread类
下面我们通过代码来看下继承Thread类创建线程的具体步骤:
首先我们需要定义一个类来集成Thread类,继承过后需要重写Thread类的run()方法,代码如下:
public class MyThread extends Thread{ @Override public void run(){ System.out.println("我是一个线程"); }}
然后我们调用该线程类对象的start()方法来启动线程,代码如下:
public static void main(String[] args){ MyThread myThread = new MyThread(); myThread.start(); }
运行结果如下:
2、实现Runnable接口
我们还可以通过实现Runnable接口来创建线程类,具体如下:
首先我们需要创建一个类来实现Runnable接口,同样我们也需要重写run()方法
public class MyRunnable implements Runnable{ @Override public void run(){ System.out.println("我是一个线程"); }}
然后我们创建Runnable实现类的对象,把这个实现类的对象作为Thread类的target对象来创建线程,最后得到的Thread类的对象才是真正的线程对象,运行start()方法来启动线程
public static void main(String[] args){ Runnable myRunnable = new MyRunnable(); Thread oneThread = new Thread(myRunnable); oneThread.start(); }
结果如下:
3、通过Callable和Future创建线程
首先创建一个类实现Callable接口,并且重写call()方法
public class MyCallable implements Callable{ private int i = 0; @Override public Integer call(){ int sum = 0; for (; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; }}
然后使用FutureTask来包装MyCallable对象,同样和上面一样把FutureTask类的对象作为Thread类的target对象来创建线程并用start()方法来启动。
public class MyThread { public static void main(String[] args){ // 创建MyCallable对象Callable myCallable = new MyCallable();//使用FutureTask来包装MyCallable对象FutureTask futureTask = new FutureTask(myCallable);for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " " + i);if (i == 3) {Thread thread = new Thread(futureTask);Thread thread = new Thread(futureTask);thread.start(); //线程进入到就绪状态}}System.out.println("主线程for循环执行完毕..");try {int sum = futureTask.get(); //取得新创建的新线程中的call()方法返回的结果System.out.println("sum = " + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}}
通过上面的例子我们发现,已经不是重写run()方法而是重写call()方法,并且还能自定义返回值,另外还有一个值得注意的问题是FutureTask实际上是实现了Runnable类并且也继承了Future接口,很显然FutureTask具有这两者的双重特征。
线程的生命周期
线程的生命周期包含新建状态、就绪状态、运行状态、阻塞状态和销毁状态等5个状态。
1、新建状态
我们使用new()方法,让它有自己的内存空间,这样new出来的就是线程的新建状态。
2、就绪状态
从上面的例子中可以看出,当我们调用start()方法时,就进入了就绪状态。注意这个时候线程不会立刻执行,而是等待CPU给他分配资源,当获得了CPU资源就开始执行,这个是面试官经常问的点。
3、运行状态
当线程获得CPU的资源时就进入了运行状态,这里我来重点说明一下运行状态的线程,面试官经常在这里做文章。
因为处于运行状态的线程是非常复杂的,它有可能会变成就绪状态、阻塞状态或者死亡状态。
当执行run()方法时,如果线程在运行过程中失去了CPU资源的时候(如调用yield()方法),该线程就会暂停运行,因为线程已经从运行状态变成了就绪状态。
那么通过哪些方式能让线程从运行状态变成就绪状态呢?
当正处在运行中的线程调用sleep方法时,会主动放弃占用的系统资源
当正处在运行中的线程调用一个阻塞式IO方法时,如果一直在等待该方法返回值的时候,那么就会造成线程被阻塞
调用yield()方法会暂停当前正在运行的线程
线程正在等待通知(notify)
调用了线程的suspend方法将该线程挂起
当然除了线程在运行状态变成就绪状态外,还能直接让运行状态的线程变成销毁状态,比如线程出现了异常或者调用了stop()、desyory()等方法。
4、阻塞状态
如果执行了sleep()方法,这时就会让线程进入阻塞状态,当然造成这种情况的不只是只有调用sleep方法,比如当等待I/O设备的时候也会造成线程进入阻塞状态。
5、销毁状态
当线程运行结束的时候,或者被强制终止的时候,线程就进入了销毁状态。当然还有我们上面提到的运行过程中发生异常的情况、JVM异常结束、调用了stop()、desyory()等方法都会使线程进入销毁的状态。注意处于销毁状态的线程是不能复活的,如果强制调用start()方法,则会抛出java.lang.IllegalThreadStateException异常。
下面附一张完整的线程生命周期图:
总结
希望通过本文能带大家更好的去了解线程技术、线程与进程的区别、线程的三个实现方法和他们的区别以及线程的生命周期,帮助大家在工作中能更好的去开发程序。