1. Java线程
讲到线程,首先需要了解进程,再来了解什么是线程。在博客线程安全中提到线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
Java语言提供了在不同硬件和操作系统平台下对线程操作的统一处理,每一个已经执行start()且还未结束的java.lang.Thread类的实例就代表了一个线程。
1.1 线程的实现
线程的实现主要由3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
1.1.1 使用内核线程实现
内核线程(KLT)就是由操作系统内核(Kernal)支持的线程。
这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口-轻量级进程(LWP)。轻量级线程即是线程,每个轻量级进程都由一个内核线程支持。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。
1.1.2 使用用户线程实现
用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程的存在。
用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。一个进程可以由多个用户线程实现,进程与用户线程之间1:N的关系称为一对多的线程模型。
1.1.3 使用用户线程加轻量级进程混合实现
在这种实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供的轻量级进程则作为用户线程和内核线程之间的栋梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。这种混合模式下,用户线程与轻量级进程的数量比是不定的,即为N:M的多对多线程模型。
1.2 Java线程的实现
由Thread类的所有关键方法都是声明为Native的可以让我们了解到Java线程的实现是平台相关的。
Java线程的实现线程模型只对线程的并发规模和操作成本产生影响。**Java线程模型为基于操作系统原生线程模型来实现的。**操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致。
Windows和Linux的JDK都是使用一对一的线程模型实现的。
2. Java线程创建方式
2.1 继承Thread类,重写run方法
下面为以继承Thread类创建一个线程的demo。这里要注意的是,除非你想重写Thread类的其他方法,以增强线程的功能,否则不建议以继承方式创建线程。
public class ThreadDemo {
private static void inheritTest(){
class SonThread extends Thread{
@Override
public void run(){
System.out.println("SonThread Execute.");
}
}
Thread oneThread = new SonThread();
oneThread.start();
}
public static void main(String[] args) {
ThreadDemo.inheritTest();
}
}
运行结果:
SonThread Execute.
2.2 实现Runable接口,重写run方法
下面为以实现Runable接口创建一个线程的demo。相较于继承Thread类创建线程,更推荐实现Runable接口创建线程。这里推荐的原因如下:可以避免单继承的限制,提高代码健壮性;更好的实现资源的共享,更利于多线程处理同一任务。
public class ThreadDemo {
private static void runableTest(){
class SonRunable implements Runnable{
@Override
public void run(){
System.out.println("SonRunable Thread Execute.");
}
}
Runnable runnable = new SonRunable();
Thread twoThread = new Thread(runnable);
twoThread.start();
}
public static void main(String[] args) {
ThreadDemo.runableTest();
}
}
运行结果:
SonRunable Thread Execute.
2.3 实现Callable、Future接口,重写call方法
下面为以实现Callable接口创建一个线程的demo。当业务需要在线程执行的过程中可以抛出异常,并在线程执行结束的时候能够返回执行结果,那么就需要实现Callable接口创建线程。Callable接口通常与线程池一起使用,ExecutorService接口中的Future<T> submit(Callable<T> task)
方法就是以Callable
类型参数,Future<T>
为返回值类型的一个submit重载方法。其中Future接口的get方法用来获取执行结果。
其中需要注意的是FutureTask作为一个同时实现Future、Runnable接口的实现类,即可以作为Runable被线程执行,又可以作为Future获取线程执行结果,线程池内部同样使用FutureTask提交并执行Callable任务。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public class ThreadDemo {
private static void callableTest() throws InterruptedException,ExecutionException{
class SonCallable implements Callable<String>{
@Override
public String call() {
System.out.println("SonCallable Thread Execute");
return "Test Down";
}
}
Callable<String> callable = new SonCallable();
RunnableFuture<String> futureTask = new FutureTask<>(callable);
Thread threeThread = new Thread(futureTask);
threeThread.start();
System.out.println(futureTask.get());
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadDemo.callableTest();
}
}
运行结果:
SonRunable Thread Execute.
Test Down
3. 守护线程与线程组
3.1 守护线程
在JVM中,Java线程被分为两类:用户线程(user thread)和守护线程(daemon thread)。
其中,守护线程设计是被用来在程序运行的过程中在后台提供一种通用服务的线程,简单来说就是为用户线程的运行提供服务。例如GC线程,在主线程运行的时候默默无闻的(其实可出名了)执行着垃圾收集工作。
守护线程和用户线程的唯一区别是:JVM退出的判断。JVM退出时不考虑守护线程的状态,只考虑用户线程的状态。当用户线程全部终止时,JVM随即退出,无论守护线程是否终止。当然,这里可以理解为被服务的对象(用户线程)没有了,提供服务的对象(守护线程)自然也没有存在的必要。这里可以进一步理解成用户线程存活时,JVM一定不会退出。还需要明白的一点是:不是用户线程存在则守护线程一定存在,守护线程在执行完自己的任务(run()方法)就会正常退出,可以比用户线程先退出。
下面根据Thread类中设置线程为守护线程的setDaemon(boolean on)的API了解守护线程相关特性。
/**
* Marks this thread as either a {@linkplain #isDaemon daemon} thread
* or a user thread. The Java Virtual Machine exits when the only
* threads running are all daemon threads.
*
* <p> This method must be invoked before the thread is started.
*
* @param on
* if {@code true}, marks this thread as a daemon thread
*
* @throws IllegalThreadStateException
* if this thread is {@linkplain #isAlive alive}
*
* @throws SecurityException
* if {@link #checkAccess} determines that the current
* thread cannot modify this thread
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
首先,通过setDaemon(true)可将线程设置为守护线程,守护线程中产生的新线程也是守护线程。可以通过isDaemon()方法检测线程是否为守护线程。
其次,setDaemon(boolean on)只能在线程未启动前调用,即只能在start()方法前调用,否则会抛出IllegalThreadStateException异常。
最后,由于守护线程可以被JVM强制终止,并且可以在任意时候被终止。所以不要在守护线程中访问文件、数据库等,否则可能会导致I/O流、数据库连接无法关闭。
3.2 线程组
当我们使用Thread类的构造方法新建一个线程时,我们可以发现在9个构造方法中有4个都需要我们传入一个ThreadGroup对象。这里的ThreadGroup就是我们要讲到的线程组的概念。
线程组简单来说就是一组线程的集合。说是一个集合,但本质上看是一颗树形结构。因为线程组还可以包括其他线程组,除了根线程组外每个线程组都有一个父节点。其中根线程组是system线程组。当使用需要传入ThreadGroup对象的构造方法时,是将新建线程加入到指定的线程组中;而不指定ThreadGroup对象时,是将新建线程加入到当前线程所在的线程组中。
线程组可以统一管理组内的线程,可以协助线程的调度。如统一设置线程优先级、中断等。一个线程可以访问自己的线程组的信息,但不允许访问有关其线程组的父线程组或任何其他线程组的信息。如果一个线程想要访问其线程组的父线程组的信息,只能通过自己的线程组访问,当前线程并不能越级访问。(这里的理解会晦涩,欢迎对于“关于一个线程可以访问自己的线程组的信息,但不允许访问有关其线程组的父线程组或任何其他线程组的信息”这句话的交流)
一般认为,提出线程组的概念是为了“安全”或者“保密”考虑。根据Arnold和Gosling的说法:“线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或者下属组之外的任何线程。”当然,更加需要关注的是Joshua Bloch的说法:“最好把线程组看成是一次不成功的尝试,你只要忽略它就好了。”所以,不建议使用线程组,对于多个线程的管理建议使用线程池。