소스 코드를 통해 Java 스레드 분석

Java에서 스레드를 생성하는 세 가지 방법

스레드 풀에 관계없이 우리 모두는 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인터페이스는 결과를 반환하고 예외를 throw할 수 있습니다.
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 소스 코드가 스레드를 생성하는 방법을 알려주는 방법을 살펴보겠습니다.

위의 3가지 메소드의 공통점은 어떤 메소드를 사용하든 쓰레드 클래스의 start() 메소드를 사용하여 쓰레드를 기동시킨다는 점이다. 먼저 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线程组。

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);
}
复制代码

image.png

通过简单的一行代码我们就得知了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使用线程组管理线程有什么好处?

  1. 方便统一管理和控制:将多个线程归为一个线程组,可以方便地统一管理和控制这些线程。比如可以统一设置线程组的优先级,同时停止或中断一个线程组中的所有线程。
  2. 更好的监控和诊断:通过线程组可以更好地监控和诊断应用程序的运行状态。可以通过线程组来获取某个线程所在的线程组,或者获取某个线程组中的所有线程信息,以便于更好地诊断线程的问题。
  3. 更高的安全性:使用线程组可以更好地限制应用程序的资源使用。比如可以设置某个线程组的最大优先级和最大线程数,以防止线程过多占用资源导致系统宕机。

总之,使用线程组可以更好地管理和控制线程,方便监控和诊断,提高系统的安全性和可靠性。

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完进行的,我们无法干预。我们可以看到这部分代码其实由两部分组成:

  1. 把线程加入到线程组中,由线程组统一管理
  2. 调用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内部维护了两个计数器nthreadsnUnstartedThreads,用于跟踪线程组内的线程数量。nUnstartedThreads 表示尚未启动的线程数,即已经创建但还没有调用 start() 方法启动的线程数。 nthreads 表示线程组中活动的线程数,包括正在执行和已经执行完成的线程数。

这些属性可以帮助线程组管理器在必要时等待线程组中的所有线程完成或终止执行。例如,当调用线程组的 join() 方法时,线程组管理器将使用 nUnstartedThreadsnthreads 属性来确定何时该方法可以返回。

大概就这么多,希望可以帮助到你。

Supongo que te gusta

Origin juejin.im/post/7215233616405577787
Recomendado
Clasificación