1. CPU、进程、线程的关系
在很久很久以前,计算机还没有操作系统,它们只能运行一个程序,这个程序直接访问机器的所有资源。
操作系统出现后,一台计算机可以运行多个程序了,它们在各自的进程(processes)中运行。每一个进程都是一个独立的运行单位,也是系统进行资源分配(比如内存、文件句柄、安全证书)和调度的基本单位。进程之间通过一些原始的机制相互通信:Socket,信号处理(signal handlers)、共享内存(shared memory)、信号量(semaphores)和文件。
后来在进程的基础上,出现了线程。线程又被称为轻量级进程(lightweight processes),一个进程可以包含多个线程。多个线程,共享进程范围内的资源,比如内存和文件句柄。前面说过操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特俗,它是被分配到线程的,线程时CPU分配的基本单位。
每一个线程都有自己的程序计数器(program counter)、栈(stack)和本地变量
程序计数器:
由于Java虚拟机的多线程是通过线程轮流切换并分配CPU执行时间的方式来实现的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行,为了能恢复到正确的执行位置,需要记住之前执行的位置,程序计数器就是做这个的。
程序计数器是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,。如果线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native方法,这个计数器的值则为空。
栈:
每个线程都有自己的栈资源,其生命周期和线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法从调用到执行完成的过程,都对应着一个栈帧在栈中入栈到出栈的过程。
本地变量
在Java中,一般指TheadLocal。
关于进程和线程的关系,如果觉得难以理解,可以点击查看:阮一峰的这篇博客
2. 线程的创建与启动
2.1 线程的创建
在Java中,使用Thread类来描述线程。创建线程就是创建Thread类,所以了解线程的创建,首先得看下Thread类的构造方法。
/**
* 1.空参构造
*/
public Thread()
/**
* 2.指定线程名称
*
* @param name 线程名称
*/
public Thread(String name)
/**
* 3.传递Runnable对象
*
* @param target Runnable对象
*/
public Thread(Runnable target)
/**
* 4.传递Runnable对象以及线程名称
*
* @param target Runnable对象
* @param name 线程名称
*/
public Thread(Runnable target, String name)
/**
* 5.指定线程名称,并将线程加入到线程组group中
*
* @param group 线程组
* @param name 线程名称
*/
public Thread(ThreadGroup group, String name)
/**
* 6.传递Runnable对象,并将线程加入到group线程组
*
* @param group 线程组
* @param target Runnable对象
*/
public Thread(ThreadGroup group, Runnable target)
...
省略两种有关线程组的构造方法。
看了Thread类的构造方法,你会发现,创建一个线程,最简单的方式就是:
Thread thread = new Thread();
是的,这就创建了一个线程,只不过当你运行这个线程的时候,它不会做任何事情,看下Thread类run()方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
逻辑非常简单,如果thread不为空,调用target.run(),这里的target是Thread的成员变量,类型为Runnable:
/* What will be run. */
private Runnable target;
看一下new Thread()方法的源码:
target为null。所以直接new Thread毫无意义,怎么改变run()方法的行为?继承。
于是出现了常见的创建线程的第一种方式:继承Thread类并重写run方法:
public class CreateThreadDemo {
public static void main(String[] args) throws Exception{
//创建线程1
MyThread myThread = new MyThread();
//启动线程
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("线程1执行了");
}
}
根据构造方法3和4,我们知道,还可以通过传递Runnable对象来创建Thread类。
Runnable是一个接口,定义了一个抽象的run()方法。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface 表名它是一个函数式接口,此处不做讨论。
创建线程的第二种方式:实现Runnable接口的run方法。相比继承,这种方式更加灵活,因为Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。
public class CreateThreadDemo {
public static void main(String[] args) throws Exception{
//创建线程2
MyThread2 myThread2 = new MyThread2();
Thread thread2 = new Thread(myThread2, "使用Runnable接口创建的线程");
//启动线程2
thread2.start();
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("线程2执行了");
}
}
上面两种方式都有一个缺点:任务没有返回值。JDK1.5以后,我们可以使用FutureTask的方式来创建线程,它可以让任务产生一个返回值。
创建线程的第三种方式:使用FutureTask
首先点开FutureTask类瞄一眼:
实现了RunnableFuture类,点开再瞄一眼:
RunnableFuture继承自Runnable,看到这里,你可能发现了:所谓使用FutureTask创建线程,其本质还是使用实现Runnable接口的run()方法的方式,就是使用构造方法3或者4:
/**
* 3.传递Runnable对象
*
* @param target Runnable对象
*/
public Thread(Runnable target)
/**
* 4.传递Runnable对象以及线程名称
*
* @param target Runnable对象
* @param name 线程名称
*/
public Thread(Runnable target, String name)
那么怎么创建FutureTask对象?看下构造方法:
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Callable}.
*
* @param callable the callable task
* @throws NullPointerException if the callable is null
*/
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Runnable}, and arrange that {@code get} will return the
* given result on successful completion.
*
* @param runnable the runnable task
* @param result the result to return on successful completion. If
* you don't need a particular result, consider using
* constructions of the form:
* {@code Future<?> f = new FutureTask<Void>(runnable, null)}
* @throws NullPointerException if the runnable is null
*/
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
FutureTask有两个构造方法:第一个传递Callable对象,第二个传递Runnable对象和FutureTask泛型的结果对象。
第二个构造方法,也是通过Executors.callable()方法将入参转为了Callable对象,此处不详细分析。
重点看第一个构造方法,它需要一个Callable对象。Callable是一个函数式接口,里面定义了一个call方法:
V call() throws Exception;
call()方法有一个返回值V,V是传递给Callable的泛型。
public class CreateThreadDemo {
public static void main(String[] args) throws Exception{
//创建线程3
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
Thread thread3 = new Thread(futureTask);
//启动线程
thread3.start();
//任务执行完毕,返回结果
String result = futureTask.get();
System.out.println(result);
}
}
class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}
2.2 启动线程
启动一个线程,调用的是start()方法,而不是run()。当我们new了一个线程对象之后,该对象处于新建状态,调用start()方法,会将新建状态变为就绪状态。
就绪状态也被称为“可执行状态”,处于该状态的线程对象,可以被分配到CPU资源,从而让run()方法得到执行。
start()和run()的区别。
启动线程为什么是调用start()而不是run()?这两个有什么区别?
在我看来,这两者的区别类似于“启动”和“执行”的区别。“启动”好比我们给汽车打火,按钮点一下,发动机便开始运作起来,我们点一下按钮后,可以去干别的,发动机的运行是异步的。“执行”是亲力亲为,心无旁骛,在执行某件事情的时候,你便只能专注于手中的事情。
在多线程中,start()类似于“启动”,它会为该线程执行必须的初始化操作,开启一个新的线程用来执行任务。
run()方法,用来描述线程的任务,它就是Thread类的一个普通public方法,你可以通过Thread对象调用它。只不过这个时候你必须等待run方法执行完毕,才能进行下面的操作。
下面用代码演示下start()方法。
public class RunAndStartDemo {
public static void main(String[] args) {
Thread printNum = new Thread(new PrintNum(),"打印数字线程");
printNum.start();
System.out.println("start 方法已经返回,当前线程name="+Thread.currentThread().getName());
}
}
class PrintNum implements Runnable{
@Override
public void run() {
System.out.println("任务开始,当前线程name="+Thread.currentThread().getName());
for (int i = 0; i < 30; i++) {
System.out.println(i);
}
System.out.println("任务结束,当前线程name="+Thread.currentThread().getName());
}
}
输出结果如下:
start 方法已经返回,当前线程name=main
任务开始,当前线程name=打印数字线程
0
1
2
3
4
... //省略后面5-29的数字
任务结束,当前线程name=打印数字线程
可以看到,start()调用之后,快速返回了,这个时候main()方法启动了一个新的线程来异步执行打印数字的任务。
在看下直接调用main()方法:
public class RunAndStartDemo {
public static void main(String[] args) {
Thread printNum = new Thread(new PrintNum(),"打印数字线程");
System.out.println("调用run方法开始,当前线程name="+Thread.currentThread().getName());
printNum.run();
System.out.println("调用run方法结束,当前线程name="+Thread.currentThread().getName());
}
}
class PrintNum implements Runnable{
@Override
public void run() {
System.out.println("任务开始,当前线程name="+Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
System.out.println("任务结束,当前线程name="+Thread.currentThread().getName());
}
}
输出结果如下:
调用run方法开始,当前线程name=main
任务开始,当前线程name=main
0
1
2
3
4
5
6
7
8
9
任务结束,当前线程name=main
调用run方法结束,当前线程name=main
可以看到,直接调用Thread类的run()方法,任务依然是由当前线程(main)驱动的,而不是开启一个新的线程。我们必须等待run()方法执行完毕才能进行下面的操作。直接调用run()方法,便达不到使用多线程的目的。