Java创建线程的三种方式
抛开线程池不谈,我们都知道在Java创建线程的方式有三种方式:
- 继承Thread类:创建一个类,继承Thread类,并重写run()方法,run()方法中的代码将在新线程中执行。然后创建该类的实例,并调用start()方法启动新线程。
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
MyThread thread = new MyThread();
thread.start();
复制代码
- 实现Runnable接口:创建一个类,实现Runnable接口,并实现run()方法,run()方法中的代码将在新线程中执行。然后创建Thread类的实例,将该类的实例作为参数传递给Thread类的构造函数,并调用start()方法启动新线程。
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
复制代码
- 使用 Callable接口 :创建一个类,实现Callable接口,并实现call()方法,call()方法中的代码将在新线程中执行,通过将
MyCallable
的实例传递给FutureTask
的构造函数创建了一个FutureTask
对象。并将FutureTask
对象传递给Thread
的构造函数创建一个线程对象。并调用start()方法启动新线程。与Runnable
接口不同,Callable
接口可以返回一个结果并且可以抛出一个异常。
Callable<Integer> callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
int result = futureTask.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码
return null;
}
}
复制代码
这三种方式在JDK层面又是如何创建线程的
我们都知道这三种方式都是JDK提供给我们创建线程的方法,我们再看一下JDK源码是如何给我们如何创建线程的。
以上三种方式都有一个共同点,无论是那种方式都是用Thread类的start()方法来启动线程,JDK启动线程的秘密肯定就在Thread类里面。我们首先来看一下Thread类,当然首先得从它的构造方法看起。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//
//省略其他构造方法
//
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
复制代码
Thread构造方法有很多,但都调用了init()
方法,也就是说创建线程对象的时候会先初始化一些东西。我们再具体看一下init()
方法,线程创建的时候有哪些初始化操作。
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
1.检查线程名是否为空,若为空则抛出空指针异常
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name.toCharArray();
Thread parent = currentThread();
//2.获取系统安全管理器,以检查当前线程是否具有足够的权限创建新线程。
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
//3.检查当前线程是否有创建新线程的权限
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
//4.线程组开始计数,未启动线程数+1
g.addUnstarted();
//5.线程组赋值
this.group = g;
this.daemon = parent.isDaemon();
//6.设置线程执行默认优先级
this.priority = parent.getPriority();
//7.设置线程实例的上下文类加载器为当前线程的上下文类加载器
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//9.设置线程的本地变量
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
//10.设置一个线程id
tid = nextThreadID();
}
复制代码
可以看到java线程初始化过程还是蛮复杂的,涉及到很多jdk底层的知识和逻辑。我们先不深究底层的东西,简单来讲java线程在初始化过程中主要做了这几件事。
- 设置线程名、线程ID、优先级、是否是守护线程等基础属性
- 设置线程的线程组
- 设置线程的类加载器
- 设置线程的本地变量(ThreadLocal)
当然其中还不乏一些权限校验。
其中比较关键的是线程的线程组、类加载器、本地变量分别在线程中起什么作用。类加载器、本地变量其实都耳熟能详,只有线程组比较陌生,我们主要来看一下Java线程组。
Java线程组(ThreadGroup)是什么,起什么作用
先来看一下ThreadGroup的源码:
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
String name;
int maxPriority;
boolean destroyed;
boolean daemon;
boolean vmAllowSuspension;
int nUnstartedThreads = 0;
int nthreads;
Thread threads[];
int ngroups;
ThreadGroup groups[];
/**
* Creates an empty Thread group that is not in any Thread group.
* This method is used to create the system Thread group.
*/
private ThreadGroup() { // called from C code
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
this.name = name;
this.maxPriority = parent.maxPriority;
this.daemon = parent.daemon;
this.vmAllowSuspension = parent.vmAllowSuspension;
this.parent = parent;
parent.add(this);
}
....
复制代码
ThreadGroup源码比较多,我们先看类定义和属性定义部分。
首先,我们看到ThreadGroup实现了Thread.UncaughtExceptionHandler接口,而Thread.UncaughtExceptionHandler接口是来处理线程在执行过程中产生的未捕获异常的。也就是说ThreadGroup可以处理线程在执行过程中产生的异常。
再是,ThreadGroup的几个关键属性,parent,groups,threads,通过属性和注释可以看出ThreadGroup是一个树形结构,有父节点有子节点,并且每一个节点里面都有一个Thread数组来存储线程。

而且我们从ThreadGroup无参构造函数可以看出,java启动的时候会创建一个叫做system的线程组,其父节点为空。除此之外ThreadGroup的parent不允许为空。换句话说,system的线程组就是所有线程组的root节点。
那么我们可以再看一下ThreadGroup有哪些关键的方法,由于代码比较多,我们主要通过注释来了解一下线程组的关键方法。
ThreadGroup.activeCount()
:返回此线程组及其子组中活动线程的估计数。ThreadGroup.activeGroupCount()
:返回此线程组中活动子组的估计数。ThreadGroup.enumerate(Thread[] list)
:将此线程组及其子组中的所有活动线程复制到指定数组中。ThreadGroup.enumerate(Thread[] list, boolean recurse)
:将此线程组中的所有活动线程复制到指定数组中,并选择是否递归复制其所有子组中的线程。ThreadGroup.enumerate(ThreadGroup[] list)
:将此线程组及其子组中的所有活动线程组复制到指定数组中。ThreadGroup.enumerate(ThreadGroup[] list, boolean recurse)
:将此线程组中的所有活动线程组复制到指定数组中,并选择是否递归复制其所有子组中的线程组。ThreadGroup.getMaxPriority()
:返回此线程组的最大优先级。ThreadGroup.getName()
:返回此线程组的名称。ThreadGroup.getParent()
:返回此线程组的父线程组。ThreadGroup.interrupt()
:中断此线程组中的所有线程。ThreadGroup.isDaemon()
:测试此线程组是否为守护线程组。ThreadGroup.setDaemon(boolean daemon)
:将此线程组设置为守护线程组或用户线程组。ThreadGroup.setMaxPriority(int pri)
:将此线程组的最大优先级设置为指定的优先级。ThreadGroup.setName(String name)
:将此线程组的名称设置为指定名称。
可以看出ThreadGroup是用来管理一组线程的,其可以中断一组线程,也可以查询到一组线程的状态,并且可以把线程重新分组。
那么我们通过以上可以推论出Java中所有线程都是由ThreadGroup来统一管理,只要我们拿到ThreadGroup对象,通过树形结构就可以对系统中所有的线程状态一目了然。
我们简单做一个实验来验证一下。 启动一个mian方法,看一下main方法的threadGroup父子节点信息。
public static void main(String[] args) {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
System.out.println(threadGroup);
}
复制代码
通过简单的一行代码我们就得知了Java启动的秘密,Java启动的时候会创建两个线程组和五个线程。分别是main线程组和mian线程,和system线程组以及如下几个线程:
- Reference Handler:负责清除 Reference 对象,它是 GC 的一部分;
- Finalizer:负责调用对象的 finalize() 方法,也是 GC 的一部分;
- Signal Dispatcher:接收操作系统信号的线程,比如 SIGTERM,SIGINT 等,用于调用 JVM 的信号处理方法;
- Attach Listener:用于接收 attach 客户端的请求,这个线程启动时会在 socket 上监听,接收到客户端请求后,会 fork 一个新的 JVM 进程,然后将客户端连接交给新进程进行处理。
这也就跟我们常说的JVM工作机制遥相呼应。
再说回来java使用线程组管理线程有什么好处?
- 方便统一管理和控制:将多个线程归为一个线程组,可以方便地统一管理和控制这些线程。比如可以统一设置线程组的优先级,同时停止或中断一个线程组中的所有线程。
- 更好的监控和诊断:通过线程组可以更好地监控和诊断应用程序的运行状态。可以通过线程组来获取某个线程所在的线程组,或者获取某个线程组中的所有线程信息,以便于更好地诊断线程的问题。
- 更高的安全性:使用线程组可以更好地限制应用程序的资源使用。比如可以设置某个线程组的最大优先级和最大线程数,以防止线程过多占用资源导致系统宕机。
总之,使用线程组可以更好地管理和控制线程,方便监控和诊断,提高系统的安全性和可靠性。
Java线程的启动过程
我们来看一下线程启动的start方法:
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 */
}
}
}
private native void start0();
复制代码
线程的start()
方法看起比init()
方法简单多了,但实际不是。启动线程是一个很复杂的过程,这部分是由JVM完进行的,我们无法干预。我们可以看到这部分代码其实由两部分组成:
- 把线程加入到线程组中,由线程组统一管理
- 调用start0()方法由JVM来操作启动线程
也就是说线程在初始化的时候会给自己指定一个线程组,在启动的时候再把自己托付给线程组。
再看一下线程组add()
,threadStartFailed()
和方法:
void add(Thread t) {
synchronized (this) {
if (destroyed) {
throw new IllegalThreadStateException();
}
if (threads == null) {
threads = new Thread[4];
} else if (nthreads == threads.length) {
threads = Arrays.copyOf(threads, nthreads * 2);
}
threads[nthreads] = t;
// This is done last so it doesn't matter in case the
// thread is killed
nthreads++;
// The thread is now a fully fledged member of the group, even
// though it may, or may not, have been started yet. It will prevent
// the group from being destroyed so the unstarted Threads count is
// decremented.
nUnstartedThreads--;
}
}
void threadStartFailed(Thread t) {
synchronized(this) {
remove(t);
nUnstartedThreads++;
}
}
复制代码
可以看到ThreadGroup
内部维护了两个计数器nthreads
和nUnstartedThreads
,用于跟踪线程组内的线程数量。nUnstartedThreads
表示尚未启动的线程数,即已经创建但还没有调用 start()
方法启动的线程数。 nthreads
表示线程组中活动的线程数,包括正在执行和已经执行完成的线程数。
这些属性可以帮助线程组管理器在必要时等待线程组中的所有线程完成或终止执行。例如,当调用线程组的 join()
方法时,线程组管理器将使用 nUnstartedThreads
和 nthreads
属性来确定何时该方法可以返回。
大概就这么多,希望可以帮助到你。