在认识线程之前首先讨论什么是并发和并行,这个问题在面试中经常被问到。这里我们引用黄俊在《深入理解Java高并发编程》中给出的定义。
并发:在有限的CPU中执行超过CPU数量的任务,任务之间交替执行。
并行: 在有限的GPU中执行任务的数量小于等于CPU的数量,这些任务同时执行。
从上面的给出的定义可以看出,并发和并行的主要区别就是执行的任务数量和CPU数量之间的关系。
1. 什么是线程?
这里引用百度百科给出线程的定义:
线程是操作系统和能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并发执行不同的任务,在Unix系统中也称为轻量进程,但轻量进程更多指的是内核线程,而把用户线程称为线程。
关于内核级线程和用户级线程的概念可以参考这篇文章。
上图左侧展示的用户级线程,可以看出线程表由进程维护,因此线程的切换由进程完成,而进程的切换由内核完成,右图展示的内核级线程,可以看到线程表由内核维护,因此线程的切换由内核完成。因此Java中的线程是内核级线程。
2. 线程的创建和启动
日程生活中,我们经常在刷微博的同时播放音乐,接下来我们将创建两个线程,一个线程用于浏览新闻,另外一个线程用户欣赏音乐。
public class TryConcurrency {
public static void main(String[] args) {
// browseNews();
// enjoyMusic();
new Thread(TryConcurrency::browseNews).start();
new Thread(TryConcurrency::enjoyMusic).start();
}
private static void browseNews() {
for (;;) {
System.out.println("Uh-huh, the good news.");
sleep(1);
}
}
private static void enjoyMusic() {
for (;;) {
System.out.println("Uh-huh, the nice music.");
sleep(1);
}
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后使用JConsole工具观察该进程的线程执行情况。
图中的Thread-0和Thread-1分别是执行browseNews和enjoyMusic函数的线程。
3. 线程生命周期
线程的生命周期分为五个阶段:NEW、RUNNABLE、RUNNING、BLOCKED、TERMINATED。
3.1 NEW阶段
Thread t1 = new Thread(() -> {
});
当一个Thread对象被创建出来后,未调用start方法前,Thread对象处于NEW阶段。
3.2 RUNNABLE阶段
t1.start();
Thread对象调用了start方法之后进入到RUNNABLE阶段,该阶段表示线程具备执行的资格,正在等待CPU的调度。当被调度器放弃执行时或主动调用yield后,从RUNNING阶段进入到RUNNALBE阶段。
3.3 RUNNING阶段
CPU通过轮询或其他方式选择了某个线程进行执行时,该线程就处于RUNNING状态。线程处于RUNNING状态表示该线程正在被CPU执行。
3.4 BLOCKED阶段
当处于RUNNING的编程执行了sleep函数或者wait函数后,线程进入BLOCKED阶段;当线程中执行了I/O操作后,也会进入BLOCKED阶段;当线程获取锁阻塞时,会进入BLOCKED阶段。
3.5 TERMINATED阶段
TERMINATED是线程的最终状态,在该状态的线程不能再切换到其他状态,当线程进入到TERMINATED状态,意味着该线程的整个生命周期都结束了。
4. Start方法源码分析
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
从start函数的代码注释可以看出,该函数的作用是触发线程执行,JVM虚拟机会调用Thread对象创建时指定的run方法。执行的结果是两个线程并发执行,一个是当前线程(执行start方法的线程),一个是执行run方法代码逻辑的线程。那么run方法是怎么被执行的呢?
我们可以看到在start方法的源码中有一个start0 JNI方法,该方法就是用来执行run方法等操作的。
private native void start0();
通过阅读start方法我们可以得出如下的结论:
- Thread被构造之后处于NEW状态,即threadStatus等0。
- 同一个Thread对象只能执行一次start方法,执行多次会抛出IllegalThreadStateException异常。
- 线程启动之后会被添加到ThreadGroup中。 Thread的run和start方法采用了模板设计模式,父类负责编写算法结构代码,子类负责实现逻辑细节。
public void run() {
if (target != null) {
target.run();
}
}