一、线程的概念
进程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的内存空间,即进程空间或虚空间。进程不依赖线程而独立存在,一个进程中可以启动多个线程。进程是系统进行资源分配和调度的基本单位。
线程
线程是指进程中一个执行流程,一个进程可以有多个线程。一个线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程中其他线程共享分配给该进程的所有资源。
线程是CPU调度和分配的基本单位,它是比进程更小的能够独立运行的基本单位。线程有自己的堆栈、程序计数器和局部变量,多个线程共享内存、文件句柄和其他每个进程应有度状态。
在Java中,每次程序运行至少启动两个线程:主线程和垃圾回收线程
二、线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程
流程图
状态说明
-
新建状态
使用
new
关键字和Thread
类或者其子类建立一个线程对象后,该线程对象就处于 新建状态。它将保持这个状态直到程序start()
这个线程。 -
就绪状态
当线程对象调用类
start()
方法之后,该线程就进入了就绪状态。就绪状态的线程处于就绪队列中,等待JVM线程调度器的调度。 -
运行状态
如果就绪状态的线程获取到CPU资源,就可以执行
run()
方法,此时线程就处于运行状态。处于运行的状态的线程最复杂,它可以变成阻塞状态、死亡状态和就绪状态。 -
阻塞状态
如果一个线程执行了
sleep(睡眠)
、suspend(挂起)
等方法,失去了所占有的资源后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或者获得设备资源后可以重新进入就绪状态。 可以分为三种:- 等待阻塞:运行状态的线程调用
wait()
方法,使线程进入等待阻塞状态; - 同步阻塞:线程获取同步锁失败(因为同步锁被其他线程占用);
- 其他阻塞:通过调用线程的
sleep()
或join()
发出I/O请求时,线程就会进入阻塞状态。当sleep()
状态超时、join()
等待看出终止或超时,或者I/O处理完毕,线程重新转入就绪状态
- 等待阻塞:运行状态的线程调用
-
死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,线程就会进入死亡状态。
三、线程的创建和启动
Java提供三种创建线程的方式
通过继承Thread类本身
代码示例
public class MyThread extends Thread {
@Override
public void run() {
super.run();
//doSomething
System.out.println(getName());
}
}
public static void main(String[] args) {
//合适的地方启动线程
new MyThread().start();
}
说明
Thread
类的实例代表着一个线程。启动线程的唯一的方法就是通过Thread
的start()
。start
是个native方法,它将启动一个线程,并执行Thread
的run
方法。
通过实现Runnable接口
代码示例
public class MyThread implements Runnable{
@Override
public void run() {
//doSomething
System.out.println("run");
}
}
public static void main(String[] args) {
//合适的地方启动线程
new Thread(new MyThread()).start();
}
说明
这个方式和上面方式本质上是一样的。Thread
本身也是一个实现了Runnable
接口的类,区别仅是使用场景的不同。我们看一下Thread
类的run
方法默认实现:
public
class Thread implements Runnable {
/* What will be run. */
private Runnable target;
//省略其他代码
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
上面的target
即是我们传入的Runnable
实现类示例,执行Thread
的run
方法即执行传入的Runnable
对象的run
方法。实际上的线程对象仍然是Thread
实例。
通过Callable和Future创建线程
代码示例
public class MyCallable implements Callable<Boolean> {
private final Object mValue;
public MyCallable(Object value) {
this.mValue = value;
}
@Override
public Boolean call() throws Exception {
boolean isString = mValue instanceof String;
if (isString){
Thread.sleep(1000);
}
System.out.println("call方法执行结束,value"+mValue);
return isString;
}
}
//合适到地方启动线程
FutureTask<Boolean> f1 = new FutureTask<>(new MyCallable("1"));
new Thread(f1).start();
FutureTask<Boolean> f2 = new FutureTask<>(new MyCallable(2));
new Thread(f2).start();
System.out.println(f1.get());
System.out.println(f2.get());
打印结果:
call方法执行结束,value2
call方法执行结束,value1
true
false
说明
-
FutureTask
类实现了Runnable
接口,所以最终还是通过Thread
来创建和启动线程,FutureTask
还是作为target被传入Thread
实例中。 -
FutureTask
内部包装了Callable
实例,执行run
方法时会执行Callable
的call
方法,并将返回值保存到FutureTask
实例的成员变量中。 -
FutureTask
的get()
方法调用,返回Callable
的call
方法的返回值,调用该返回将会导致线程(方法调用的线程)阻塞,必须等到子线程结束后才会得到返回值。 -
FutureTask
的get(long timeout, TimeUnit unit)
方法调用,返回Callable
的call
方法的返回值。该方法调用最多阻塞线程timeout
和unit
指定的时长;如果超过指定时长call
方法还没有执行完成,则会抛出TimeoutException
异常。
其他FutrueTask
类方法说明可查看:FutrueTask类API文档
总结
-
虽然从形式上有三种创建线程的方式,但是本质上最后还是要通过
Thread
类来创建线程,三种方法的区别仅上run()
方法内实际执行线程逻辑的执行体的区别;不管使用哪种方式,最终均需要调用Therad
类的start()
方法,使线程进入就绪状态。 -
实际开发中我们一般使用实现
Runnable
接口的方式(或者方式三)来实现多线程编程。有如下好处:(1).避免继承的局限;(2).接口实现便于资源共享(不是说方式一不能实现资源共享,仅仅是接口实现方便一点,可以通过不同的线程对象传入同一的Runnalbe
接口实例的方式共享资源)。