前言
说到Java的运行,就离不开进程和线程,这两个概念其实也是所有编程语言的核心。无论是什么语言,归根结底就是在操作CPU,让CPU分配相应的计算资源给应用程序。而线程就是CPU能够调度和分派的基本单元,多个线程组合在一起做一件事情就变成了进程。线程可以说是Java中基础的基础,但是由于其不好理解因此很少会被当作Java的基础去介绍和讲解。这篇博客会对Java中线程的使用和部分方法源码做一个整理,对自己是一个基础巩固,顺带希望能帮助其他人更好的理解线程。更多线程知识内容请点击【Java 多线程和锁知识笔记系列】
Thread类
Thread类是Java线程相关的类中的实现类,多数线程有关的类或者接口都得依赖这个类中提供的方法进行,使用方法如下:
public class Thread1 extends Thread{
public static void main(String[] args) {
Thread thread=new Thread();
thread.start();
}
@Override
public void run() {
System.out.println("thread1 test");
}
}
一旦调用start()
方法就会开启一个线程,执行run()
方法内部的逻辑。
Runnable接口
Runnable接口也是一个线程相关的接口,里面只有一个run()方法。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
上面说Thread的时候,说到线程被启动了就执行了run()
方法的逻辑。那么这两个run()
方法有什么联系呢?答案是两个run()
方法其实是一样的。因为Thread类也实现了Runnable接口,Thread里面的run()
方法就是Runnable里面的run()
方法。可以说当继承并重写(override) Thread类中的run()
方法的时候,就是在写Runnable.run()
,毕竟父类的实现,子类也可以重做。
public class Thread implements Runnable { ...... }
由于Runnable只是一个接口,因此它的使用,必须借助Thread类实现,比如。
public class Runnable1 implements Runnable{
@Override
public void run() {
System.out.println("Runnable1 test");
}
public static void main(String[] args) {
new Thread(new Runnable1()).start();
}
}
这也就是为什么很多博客里说实现了Runnable接口的类,是一个假的线程类,只是实现了run()方法而已。因为不使用Thread之类的线程类辅助创建线程,那么实现了Runnable接口,也只是实现了Runnable接口而已和线程不沾边。
线程的生命周期
刚才通过介绍Thead类和Runnable接口的例子,可以看出线程是有开始的,又开始就得有结束,也会有阻塞等等一系列的方法去修饰线程,这些加在一起被统称为线程的生命周期。人的生命周期有生老病死,线程的生命周期也有生老病死等等一系列的状态,去描述一个线程从创建到销毁的过程。
生命周期状态
下面是在Thread类中定义的生命周期状态,以下英文来自Thread类注释:
状态名字 | 中文名 | 源码注释 | 翻译和描述 |
---|---|---|---|
NEW | 初始化状态 | A thread that has not yet started is in this state. | 刚刚被创建new Thread(),但是还没有调用start()方法。 |
RUNNING | 运行状态 | N/A | 线程获取时间片,正在运行,Runnable的子状态。 |
READY | 就绪状态 | N/A | 线程已经初始化,调用start(),但是还没有分配CPU时间片运行,Runnable的子状态。 |
RUNNABLE | 可运行状态(包括READY和RUNNING) | A thread executing in the Java virtual machine is in this state. Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor. | 在Java虚拟机中正在执行的线程就是处于这种状态。但是这个线程可能是正在执行(Running),也能是正在等待操作系统其他资源(比如处理器)的一个线程(Ready)。 |
BLOCKED | 阻塞状态 | Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling {@link Object#wait() Object.wait}. | 表示被锁阻塞住的线程。处于这个状态的线程一般是等待一个进入synchronized块/方法的锁,或者在调用Object.wait ()之后重新进入synchronized块/方法。 |
WAITING | 等待状态 | Thread state for a waiting thread. A thread is in the waiting state due to calling one of thefollowing methods: {@link Object#wait() Object.wait} with no timeout {@link #join() Thread.join} with no timeout {@link LockSupport#park() LockSupport.park} | 线程进入等待状态说明当前线程调用了Object.wait()、Thread.join()、LockSupport.park()方法,并且都没有设置timeout时间。 |
TIME_WAITING | 超时等待状态 | Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time: {@link #sleep Thread.sleep} {@link Object#wait(long) Object.wait} with timeout {@link #join(long) Thread.join} with timeout {@link LockSupport#parkNanos LockSupport.parkNanos} {@link LockSupport#parkUntil LockSupport.parkUntil} | 这个和上面的类似,只不过这些调用的方法都有一个明确的等待时间,调用下面这些方法Thread.sleep()、Object.wait(long)、Thread.join(long)、 LockSupport.parkNanos()、LockSupport.parkUntil(),可以转入这个状态 |
TERMINATED | 终止状态 | Thread state for a terminated thread. The thread has completed execution. | 处于这个状态的线程究竟完成了执行,可以被终止了。 |
状态交替
既然线程的生命周期有各种状态,那么他们之间转换我们可以用一张图来表示。
从上面的图里可以看到实例化之后就会转到NEW
,然后调用start()
方法就转化到了RUNNABLE
状态,然后在RUNNING
和READY
就是在run()
方法里面的逻辑,当CPU分配到时间片的时候,会进入了RUNNING
状态。而WAITING
、BLOCKED
、TIME_WAITING
则均为挂起的状态。
线程执行的顺序
从线程的生命周期中可以看到,当线程进入RUNNABLE
状态以后,线程的执行基本上是看脸吃饭了。因为CPU并不会按照创建顺序去执行不同的线程,而是一个随机的过程,虽然可以设置优先级,但是优先级也仅仅只是一个参考。即便设置了,相同优先级下哪个线程先执行也是不确定的,比如下面的例子。
public class ThreadOrder{
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println("t1");
});
Thread t2=new Thread(()->{
System.out.println("t2");
});
Thread t3=new Thread(()->{
System.out.println("t3");
});
t1.start();
t2.start();
t3.start();
}
}
例子中t1、t2、t3的顺序每次都不固定,因为在多线程的情况下,CPU并不会按照他们的调用顺序分配时间分片,其执行的流程图为:
为了保证这些线程顺序执行,可以使用join()
方法,修改程序为下面的样子,再次运行。无论多少次都是按照t1、t2、t3的顺序执行的。
public class ThreadOrder{
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
System.out.println("t1");
});
Thread t2=new Thread(()->{
System.out.println("t2");
});
Thread t3=new Thread(()->{
System.out.println("t3");
});
t1.start(); t1.join(); //对每个线程调用join()方法
t2.start(); t2.join();
t3.start(); t3.join();
}
}
为什么加入了join()
方法就会导致线程执行有顺序呢?这就需要进入join()
的源码里看了。
public final void join() throws InterruptedException {
join(0);
}
继续进入join(0)
方法。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
首先这个方法被synchronized
关键字修饰了,可以看作执行到join()
的时候资源是被锁住的。而外部传递的是0
这个值,进入到方法里。当millis == 0
且当前线程仍然活着的时候isAlive()
,就会执行wait(0)
,这个方法被native
修饰是个本地方法,所以这里wait
的并不是子线程,而是主线程。
public final native void wait(long timeout) throws InterruptedException;
也就意味着当子线程调用join()
方法以后,主线程就会wait()
,那么其执行的流程就和修改前的大大的不一样了:每次join()
主线程必须让出CPU时间片给子线程,等待子线程执行完毕,才能接着执行。就意味着t1,t2,t3的启动执行被固定了,因此总是有序的执行。修改后流程图如下:
总结
本篇主要讲了线程Thread类,Runnable接口的使用,线程的生命周期,如何保证线程的顺序,以及join()方法的源码和原理。由于Thread是类,Runnable是接口,一般会推荐使用Runnable接口来实现线程,因为Runnable接口比Thead是有优势的。
- 语法上Runnable是接口,就可以避免Java的单继承特性带来的局限性。
- 复用性上使用接口实现的类,能够使多个线程共享这部分代码,可以做到代码数据独立,增加程序的健壮性。
- 当要是用线程池或者Callable的时候,由于只留有Runnable接口的参数,因此只能使用Runnable,而不能使用继承Thread的类。
但无论是Thread类或者Runnable接口,其run()方法都没有返回值。有些时候希望线程有返回值的时候就需要用到Callable接口,那么下一篇【Java 线程知识笔记 (二) Callable与Future】就会重点讲Callable接口和Future接口的使用。