通过源码来刨析Java线程

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
  • @param acc the AccessControlContext to inherit, or

*/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线程组(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工作机制遥相呼应。

作者:猫清扬链接:https://juejin.cn/post/7215233616405577787来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

抛开线程池不谈,我们都知道在Java创建线程的方式有三种方式:

  1. 继承Thread类:创建一个类,继承Thread类,并重写run()方法,run()方法中的代码将在新线程中执行。然后创建该类的实例,并调用start()方法启动新线程。

public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

MyThread thread = new MyThread();
thread.start();
复制代码

  1. 实现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();

复制代码

  1. 使用 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线程在初始化过程中主要做了这几件事。

  1. 设置线程名、线程ID、优先级、是否是守护线程等基础属性
  2. 设置线程的线程组
  3. 设置线程的类加载器
  4. 设置线程的本地变量(ThreadLocal)

当然其中还不乏一些权限校验。

其中比较关键的是线程的线程组类加载器本地变量分别在线程中起什么作用。

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线程组以及如下几个线程:

  1. Reference Handler:负责清除 Reference 对象,它是 GC 的一部分;
  2. Finalizer:负责调用对象的 finalize() 方法,也是 GC 的一部分;
  3. Signal Dispatcher:接收操作系统信号的线程,比如 SIGTERM,SIGINT 等,用于调用 JVM 的信号处理方法;
  4. Attach Listener:用于接收 attach 客户端的请求,这个线程启动时会在 socket 上监听,接收到客户端请求后,会 fork 一个新的 JVM 进程,然后将客户端连接交给新进程进行处理。

这也就跟我们常说的JVM工作机制遥相呼应。

#java##后端##java研发#
全部评论

相关推荐

11-08 10:39
门头沟学院 C++
点赞 评论 收藏
分享
10-28 14:42
门头沟学院 Java
watermelon1124:因为嵌入式炸了
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务