操作系统中,一个进程往往代表着一个应用程序实例,而线程是进程中轻量级的调度单元,也可以看作是轻量级的进程,可以共享进程资源。下面简单介绍在操作系统中线程通用实现方式。接下来内容主要对线程模型进行简单介绍,然后对Java线程实现Thread类进行了解。
暂且抛开Java线程,先说明一下在操作系统中,线程通用的几种实现方式。实现线程主要有三种方式。
使用内核线程实现的方式,通常也被成为1 : 1实现模型。内核线程(Kernel Level Thread,KLT)是直接由操作系统内核来支持的线程,这种线程由内核来控制切换,内核通过调度器(Scheduler)来对线程进行调度,并负责将线程任务映射到各个处理器,在多核操作系统中具有能力并行处理多个任务,这种支持多线程的内核被称为多线程内核(Mutil-Threads Kernel)。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口(轻量级进程Light Weight Process)LWP,也就是通常意义所描述的线程,每个轻量级进程都由一个内核支持,因此先支持内核线程,才能有轻量级进程。其中KLT、Schedule、LWP之间关系如下图所示:
在这个模型中,每个轻量级进程都是一个调度单元,单个KLT的阻塞不会影响其他单元的调度,当然也有其本身的局限性,由于是基于内核线程进行创建的,所以线程的各种操作,如创建、同步等都需要进行系统调用。应用程序运行在用户空间,内核处于用户态(User Mode),系统调用需要进行用户态和内核态(Kernel Mode)切换,频繁的进行用户态内核态切换,会严重消耗系统性能。此外,每个KLT都需要内核来支持,因此也会消耗内核资源如内核线程空间等,因此这种模型下所能创建的线程数也是很有限的。
使用用户线程实现的方式被称为1 : N模型,非内核线程都可以被看作是用户线程(User Thread,UT)的一种。这里需要辩证的看待关于用户线程的定义,广义上来从非内核线程的角度看,轻量级进程也属于用户线程的范畴,但是由于其建立在内核基础之上,过于依赖系统调用,又不具备通常意义上用户线程的优点。其实现模型如图所示:
狭义上所说的用户线程指完全建立在用户空间的线程,线程的控制无需内核参与,内核也无法感知其实现模式,这种线程也不需要进行用户态和内核态的切换,因此用户线程对资源的使用率较小,支持大规模的线程数量。部分高性能数据库中的线程就是完全由这种类型的用户线程来实现。
用户线程的优势也是其劣势,由于没有内核的支持,所有线程都需要由用户程序处理。操作系统通常只会进行进程级别资源的分配,那么在用户空间,用户线程如何创建、销毁、切换、阻塞以及调度都要由用户来处理,因此用户线程实现的程序也提高了复杂度和故障机率,除非是大规模的线程需要场景下,否则一般不倾向于使用用户线程。
Java中的线程曾经使用用户线程实现,最终由于其复杂度被放弃。近年来,以高并发为特性的语言,如Golang、Erlang中使用用户线程的场景有所提升。
基于使用内核线程和用户线程的模型,还有一种就是整合两种模型特性的混合实现模型,也被成为M : N模型。这种模型中即使用了内核线程,也使用了用户线程,通过轻量级进程作为用户线程和内核线程之间的桥梁,既可以使用内核提供的线程进行调度及处理器映射,同时也兼具了大规模用户线程并发。这种模型下用户线程和轻量级进程的数量比不固定,如下图所示:
在许多UNIX类操作系统,如Solaris、HP-UX等都提供了这种M : N的线程模型实现。
Java虚拟机规范中并没有定义关于线程如何实现,因此不同虚拟机厂商实现方式也没有统一标准。在JDK1.2前的线程,早期的Classic虚拟机中,基于一种被称为绿色线程的用户线程来实现。但在JDK1.3之后,商用Java虚拟机普遍开始使用内核线程模型,即1 : 1模型来作为Java线程的实现。
在HotSpot虚拟机中,每个Java线程都是直接映射到操作系统的原生线程来实现,虚拟机本身并不会干预线程的创建、调度,但是可以设置线程的优先级给予操作系统建议。关于线程的冻结或者唤醒、线程由那颗CPU核心执行以及可使用的CPU执行时间片都由操作系统来完成。
当然也有特殊场景,用于JavaME的CLDC HotSpot Implementation同时支持1 : N以及特殊的混合模型。Solaris平台的HotSpot,由于操作系统的特性,因此同时支持1 : 1和N : M的两种线程模型,通过虚拟机参数设置。
总体来说,Java线程模型的实现通常依赖于所运行的操作系统内核提供的支持,在不同平台并不能达成一致,因此Java虚拟机规范也未明确的做出定义。另外,至于使用何种线程模型,其所影响的只有线程规模(线程数量)和操作成本。
线程无论基于何种模型创建,都有其调度策略,线程的调度指的是操作系统为线程分配使用权的过程。通常调度方式包含两种,分别是协同式(Cooperative Threads Scheduling)和抢占式(Preemptive Threads Scheduling):
使用协同式调度方式的线程调度由其本身来控制,线程在自身工作执行完成后,主动通知系统切换到另一个线程执行,这种方式实现简单,便于控制。但是过于依赖线程本身来控制调度,如果某个线程执行任务的程序存在问题就会导致一直阻塞。
2. 抢占式调度
使用抢占式调度方式的多线程系统,线程的调度由系统分配执行时间,线程的切换由系统决定。这种调度方式下,线程的执行时间可控,不会因单个线程问题导致应用程序堵塞。Java中所使用的线程调度策略就是抢占式,虽然整个调度基于系统来确定,但是可以通过设置优先级的方式给予操作系统一定的建议,总共包含1~10优先级,优先级越高,线程越容易被选择执行。
Java语言是支持多线程的,一个正在运行的Java程序可以称之为一个进程(process),在每个进程里面包含多个线程,线程是进程中单一的顺序控制流,CPU在执行计算机指令的时候都是按顺序执行,但是由于其执行速度很快,可以把时间分成很细小的时间片,交替执行,线程和进程的区别在于:
Thread类位于java.lang包,JDK1.0引入。在HotSpot虚拟机中,线程使用的是基于操作系统的1 : 1的内核实现模型来创建线程,线程的创建、调度、执行、销毁等由内核进行控制,调度过程通过抢占式策略进行调度。
Java语言定了线程生命周期的6种状态,在任意时刻线程只能处于一种状态,Java中提供了特定的API,在某些场景下可以实现线程间状态的转换。这6种状态使用枚举类java.lang.Thread.State来表示,每种状态如下所示:
线程的生命周期以及状态转换如下图所示:
接下来对Thread对象的声明周期包含的六种状态进行进行简单模拟。
public class Test{
public static void main(String[] args) throws Exception{
System.out.println("Thread State is:"+new Thread().getState());
}
}
输出结果
Thread State is:NEW
public class Test implements Runnable{
@Override
public void run() {
System.out.println("Thread State is:"+Thread.currentThread().getState());
}
public static void main(String[] args) throws Exception{
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
}
}
输出结果
Thread State is:RUNNABLE
start()
方法,T1,T2状态均为RUNNABLE
RUNNABLE
,T2状态变为BLOCKED
RUNNABLE
class BlockThread extends Thread {
private String name; //当前线程名称
private Object lock; //锁
public BlockThread(Object lock,String name){
this.lock = lock;
this.name = name;
}
@Override
public void run() {
System.out.println("Thread "+name+" State is "+Thread.currentThread().getState());
synchronized (lock) {
System.out.println("Thread "+name+" hold the lock");
try {
System.out.println("Thread "+name+" State is "+Thread.currentThread().getState());
Thread.sleep(1000 * 10); //抢到锁的线程执行逻辑,这里用睡眠模拟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + name + " release the lock");
}
}
}
public class Test{
public static void main(String[] args) throws Exception{
Object lock = new Object();//锁
BlockThread t1 = new BlockThread(lock,"T1");
BlockThread t2 = new BlockThread(lock,"T2");
t1.start(); //线程 T1开始运行
t2.start(); //线程 T2开始运行
Thread.sleep(100); //阻塞主线程,等待T1,T2抢锁
System.out.println("Thread T1 State is " + t1.getState()); //获取T1线程状态
System.out.println("Thread T2 State is " + t2.getState()); //获取T2线程状态
}
}
输出结果
Thread T1 State is RUNNABLE
Thread T1 hold the lock
Thread T1 State is RUNNABLE
Thread T2 State is RUNNABLE
Thread T1 State is TIMED_WAITING
Thread T2 State is BLOCKED
Thread T1 release the lock
Thread T2 hold the lock
Thread T2 State is RUNNABLE
Thread T2 release the lock
class WaitingThread extends Thread {
private Object lock;
public WaitingThread(String name, Object lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName()+" try to wait");
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test{
public static void main(String[] args) throws Exception {
Object lock = new Object();
WaitingThread t = new WaitingThread("T", lock);
t.start();
Thread.sleep(1000);
System.out.println("Thread T State is " + t.getState());
System.out.println("Thread "+Thread.currentThread().getName()+" State is " + Thread.currentThread().getState());
}
}
输出结果
Thread T try to wait
Thread T State is WAITING
Thread main State is RUNNABLE
线程调用sleep()
方法,进入TIMED_WAITING状态
class WaitingThread extends Thread {
private Object lock;
public WaitingThread(String name, Object lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test{
public static void main(String[] args) throws Exception {
Object lock = new Object();
WaitingThread t1 = new WaitingThread("T1", lock);
WaitingThread t2 = new WaitingThread("T2", lock);
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("Thread T1 State is " + t1.getState());
System.out.println("Thread T2 State is " + t2.getState());
}
}
输出结果
Thread T1 State is TERMINATED
Thread T2 State is TIMED_WAITING
public class Test implements Runnable{
@Override
public void run() {
}
public static void main(String[] args) throws Exception{
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
System.out.println("Thread State is "+thread.getState());
}
}
输出结果
Thread State is TERMINATED
Java中的线程相关操作由Thread类实现,Thread类位于java.lang包,于JDK 1.0版本引入。Thread类封装了多线程操作的上层API,可执行单元代码逻辑通过实现Runnable接口,重写run方法完成,Thread类本身也实现了该接口,接口定义如下所示:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
在JDK1.8 引入函数式编程后,Runnable接口也被@FunctionalInterface注解标记为函数式接口,支持Lambda表达式。在JDK1.8中,Thread类提供了如下构造方法:
说明:关于线程组(ThreadGroup类),一个线程组代表一组线程。此外,一个线程组还可以包括其他线程组。线程组形成一棵树,其中除了初始线程组之外的每个线程组都有一个父级。允许线程访问有关其自己的线程组的信息,但不能访问有关其线程组的父线程组或任何其他线程组的信息。
Java中创建的线程,每个线程都有一个优先级,具有较高优先级的线程优先于具有较低优先级的线程执行。当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为等于创建线程的优先级。Thread类中定义了以下三个默认优先级:
// 线程所能设置的最小优先级
public final static int MIN_PRIORITY = 1;
// 创建线程的默认优先级
public final static int NORM_PRIORITY = 5;
// 线程所能设置的最大优先级
public final static int MAX_PRIORITY = 10;
Java中如何创建一个线程Thread,可以继承Thread类、实现Runnable或者Callable接口进行显式的创建线程。需要注意的是Runnable接口只是提供了自定义任务执行单元的方法逻辑,而真正关于线程的上层API操作实现还是位于Thread类。查看Thread中重写的run方法源码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
虽然Thread类实现了run方法,但是并没有做任何的特殊处理,真正的任务执行单元,会交给构造方法传入的可执行对象完成逻辑处理,这里使用的是模板方法设计模式。
关于Callable接口下面会详细介绍。
通过继承Thread类,并重写run方法。代码如下:
public class Test extends Thread{
@Override
public void run() {
System.out.println("Thread is Created");
}
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
System.out.println(Thread.currentThread().getName());
}
}
在主线程创建新的线程,二者的执行时机由系统调度确定,因此输出结果是随机的,如下所示:
/*新建线程先执行*/
Thread is Created
main
/*主线程先执行*/
main
Thread is Created
通过实现Runnable接口并且重写run方法来创建一个线程。代码如下
public class Test implements Runnable{
@Override
public void run() {
System.out.println("Thread is Created");
}
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
System.out.println(Thread.currentThread().getName());
}
}
输出结果
/*新建线程先执行*/
Thread is Created
main
/*主线程先执行*/
main
Thread is Created
严格来讲,参考Oracle官方文档,创建一个Thread只有这两种方式,无论实现Runable接口,还是继承Thread类,都存在一些缺陷,我们无法获得线程的执行结果,无法处理执行过程的异常,这里提供另外一种创建线程的方式。
Callable是JDK 1.5新增的接口,位于java.util.concurrent
包下,Callable接口里面定义了call方法,call方法是run方法的增强版,可以通过实现Callable接口时传入泛型来指定call方法的返回值,并且可以声明抛出异常。
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
Thread中无论哪一种构造方法都没有Callable类型的target,只能传入Runnable类型的target,那如何把Thread和Callable联系起来,这里就要引入Future
接口和FutureTask
实现类。
Future接口定义如下方法
/**尝试取消执行此任务。*/
boolean cancel(boolean mayInterruptIfRunning)
/**等待计算完成,然后检索其结果。*/
V get()
/**如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。*/
V get(long timeout, TimeUnit unit)
/**如果此任务在正常完成之前被取消,则返回 true 。*/
boolean isCancelled()
/**返回 true如果任务已完成。*/
boolean isDone()
查看源码,RunnableFuture作为一个过渡同时继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
public interface RunnableFuture extends Runnable, Future {
void run();
}
FutureTask类提供了两个构造方法
再回到最初的问题如何将实现了Callable接口的线程类作为Thread实例的target,这里经过了以下过程
Future接口和FutureTask类在中间做了一层包装,代码展示如下
public class Test implements Callable {
@Override
public String call() throws Exception {
System.out.println("Thread is Created");
return "OK";
}
public static void main(String[] args) throws Exception {
Test test = new Test();
FutureTask futureTask = new FutureTask(test);
Thread thread = new Thread(futureTask);
thread.start();
String str = (String) futureTask.get(5,TimeUnit.SECONDS);
System.out.println(str);
System.out.println(Thread.currentThread().getName());
}
}
输出结果
Thread is Created
OK
main
Java中线程采用内核线程模型来实现用户程序中的线程,因此一些常用方法依托于虚拟机原生实现,下面介绍这些native方法的基本使用。
yield方法是一个native方法,由C++底层进行关于操作系统层面的逻辑处理。yield的字面意思是退让。调用该方法会向调度程序提示当前线程愿意放弃其当前对处理器的使用,调度程序可以随意忽略此提示。
yield是一种启发式尝试,使用它可以改善线程之间的相对进展,否则会过度使用 CPU。在使用yield方法时通常有下面两种使用场景:
如下代码所示
public class TestYield {
public static void main(String[] args) {
MyThread thread1 = new MyThread("thread-1");
MyThread thread2 = new MyThread("thread-2");
thread1.start();
thread2.start();
}
private static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
if (i % 2 == 0) {
Thread.yield();
System.out.println(getName() + ":" + i);
}
}
}
}
}
join方法让一个线程加入到另一个线程之前执行,在此线程执行期间,其他线程进入阻塞状态,当然也可以指定join入参(指定执行等待的超时时间),最多等待几毫秒让该线程终止,超时0意味着永远等待。
此实现使用以this.isAlive为条件的this.wait调用循环,当线程终止时,将调用this.notifyAll方法。建议应用程序不要在Thread实例上使用wait、notify或notifyAll。如果任何线程中断了当前线程,会抛出InterruptedException异常时清除当前线程的中断状态。
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread("thread-1");
MyThread thread2 = new MyThread("thread-2");
thread1.start();
thread1.join();
thread2.start();
}
private static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName());
}
}
}
}
当调用线程的sleep方法,使当前执行的线程休眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性。如果任何线程中断了当前线程,会抛出InterruptedException异常时清除当前线程的中断状态。
使用interrupt方法会中断这个线程,除非当前线程正在中断自己,否则会调用该线程的checkAccess方法,这可能会导致抛出SecurityException。主要有以下几种场景:
public class TestInterrupt {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread");
thread.start();
thread.interrupt();
}
}
输出结果
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.starsray.test.api.TestInterrupt.lambda$main$0(TestInterrupt.java:8)
at java.lang.Thread.run(Thread.java:748)
这里就不一一演示,列举在Thread中提供的其他方法。
activeCount()
:返回当前线程的thread group及其子组中活动线程数的估计checkAccess()
:确定当前正在运行的线程是否有权限修改此线程clone()
:将CloneNotSupportedException作为线程抛出无法有意义地克隆currentThread()
:返回对当前正在执行的线程对象的引用dumpStack()
:将当前线程的堆栈跟踪打印到标准错误流enumerate(Thread[] tarray)
:将当前线程的线程组及其子组中的每个活动线程复制到指定的数组中getAllStackTraces()
:返回所有活动线程的堆栈跟踪图getContextClassLoader()
:返回此Thread的上下文ClassLoadergetDefaultUncaughtExceptionHandler()
:返回线程由于未捕获异常突然终止而调用的默认方法getId()
:返回此线程的标识符getName()
:返回此线程的名称getPriority()
:返回此线程的优先级getStackTrace()
:返回表示此线程的堆栈转储的堆栈跟踪元素数组。getState()
:返回此线程的状态getThreadGroup()
:返回此线程所属的线程组getUncaughtExceptionHandler()
:返回由于未捕获的异常,此线程突然终止时调用的处理程序holdsLock(Object obj)
:返回 true当且仅当当前线程在指定的对象上保持监视器锁interrupt()
:中断这个线程interrupted()
:返回当前线程是否中断isAlive()
:返回这个线程是否活着isDaemon()
:返回这个线程是否是守护线程isInterrupted(boolean ClearInterrupted)
:测试某个线程是否已被中断setContextClassLoader(ClassLoader cl)
:设置此线程的上下文ClassLoader。setDaemon(boolean on)
:将此线程标记为 daemon线程或用户线程。Java中线程除了用户工作线程外还有一种特殊的线程被称为守护线程。通过setDaemon(true)方法将此线程标记为守护线程或用户线程。当虚拟机中唯一运行的线程都是守护线程时,Java虚拟机退出。该方法必须在线程启动前调用。
守护线程最主要的作用就是服务于虚拟机中的非守护线程,比如虚拟机中的垃圾回收线程就是典型的守护线程。需要注意的是如果是在守护线程中产生的线程,那么这个线程依然是守护线程。如代码所示:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class TestDaemon {
private static volatile boolean restFlag = false;
private static final int threshold = 5;
public static void main(String[] args) throws InterruptedException, IOException {
List list = new ArrayList<>();
// 工作线程
Thread worker = new Thread(() -> {
while (!restFlag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!restFlag) {
list.add(1);
System.out.println("worker is working");
}
}
System.out.println("worker rest");
}, "work-thread");
// 守护线程
Thread employers = new Thread(() -> {
while (!restFlag) {
if (list.size() >= threshold) {
restFlag = true;
System.out.println("employers exit");
}
}
}, "employers-thread");
employers.setDaemon(true);
worker.start();
employers.start();
}
}
Java中线程使用的是与操作系统1 : 1的内核线程模型,因此线程的创建、运行等依赖于操作系统的调度。线程生命周期中包含六种状态,线程调用不同的方法可以在RUNNING、WATING、TIMED_WAITING、BLOCK之间相互转换。
这里总结一下三种创建线程的方法特点
一般推荐使用以实现接口的方式创建线程类。