Java高并发编程基础之Thread构造函数大有内涵

引言

在Java中,Thread类提供了许多丰富的构造函数,以便于创建和管理线程。使得可以根据具体需求来创建和配置线程对象,从而实现更灵活、可扩展的多线程编程。

  • Thread类的无参构造函数可以创建一个新的线程对象,然后通过调用start()方法来启动线程的执行。
  • Thread类提供了其他一些常用的构造函数。例如,可以使用带有Runnable参数的构造函数来创建一个线程对象,并传入一个实现了Runnable接口的对象,从而指定线程要执行的任务。这种方式更常用,因为它可以使得代码更具可重用性和灵活性。
  • Thread类提供了带有线程名称、线程优先级等参数的构造函数,可以通过这些构造函数来设置线程的属性。
  • 使用带有ThreadGroup参数的构造函数将线程添加到特定的线程组中。线程组可以方便地对一组线程进行管理和控制。

Thread的构造函数

    public Thread() {
        this(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }

    Thread(Runnable target, @SuppressWarnings("removal") AccessControlContext acc) {
        this(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
    }

    public Thread(ThreadGroup group, Runnable target) {
        this(group, target, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(String name) {
        this(null, null, name, 0);
    }

    public Thread(ThreadGroup group, String name) {
        this(group, null, name, 0);
    }

    public Thread(Runnable target, String name) {
        this(null, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name) {
        this(group, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        this(group, target, name, stackSize, null, true);
    }

    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize, boolean inheritThreadLocals) {
        this(group, target, name, stackSize, null, inheritThreadLocals);
    }

线程的命名

在构造线程的时候可以为线程起一个有特殊意义的名字,这也是比较好的一种做法。尤其在一个线程比较多的程序中,为线程赋予一个包含特殊意义的名字有助于问题的排查和线程的跟踪,强烈推荐在构造线程的时候赋予它一个名字。

线程的默认命名

使用Thread有的构造函数没有提供名称的参数,这个时候系统会生成一个默认的线程名称。默认的名称都是 Thread-加上线程编码数字 。以下是比较常用的不带线程名称的构造器函数。

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, null, gname)}, where {@code gname} is a newly generated
     * name. Automatically generated names are of the form
     * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
     */
    public Thread() {
        this(null, null, "Thread-" + nextThreadNum(), 0);
    }

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, target, gname)}, where {@code gname} is a newly generated
     * name. Automatically generated names are of the form
     * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this classes {@code run} method does
     *         nothing.
     */
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (group, target, gname)} ,where {@code gname} is a newly generated
     * name. Automatically generated names are of the form
     * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
     *
     * @param  group
     *         the thread group. If {@code null} and there is a security
     *         manager, the group is determined by {@linkplain
     *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
     *         If there is not a security manager or {@code
     *         SecurityManager.getThreadGroup()} returns {@code null}, the group
     *         is set to the current thread's thread group.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this thread's run method is invoked.
     *
     * @throws  SecurityException
     *          if the current thread cannot create a thread in the specified
     *          thread group
     */
    public Thread(ThreadGroup group, Runnable target) {
        this(group, target, "Thread-" + nextThreadNum(), 0);
    }

命名线程

构造函数提供了name作为线程名称的参数用于初始化构造函数,开发过程提供名称更利于问题的排查。

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, null, name)}.
     *
     * @param   name
     *          the name of the new thread
     */
    public Thread(String name) {
        this(null, null, name, 0);
    }
/**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (group, null, name)}.
     *
     * @param  group
     *         the thread group. If {@code null} and there is a security
     *         manager, the group is determined by {@linkplain
     *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
     *         If there is not a security manager or {@code
     *         SecurityManager.getThreadGroup()} returns {@code null}, the group
     *         is set to the current thread's thread group.
     *
     * @param  name
     *         the name of the new thread
     *
     * @throws  SecurityException
     *          if the current thread cannot create a thread in the specified
     *          thread group
     */
    public Thread(ThreadGroup group, String name) {
        this(group, null, name, 0);
    }

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, target, name)}.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this thread's run method is invoked.
     *
     * @param  name
     *         the name of the new thread
     */
    public Thread(Runnable target, String name) {
        this(null, target, name, 0);
    }

    /**
     *
     * @param  group
     *         the thread group. If {@code null} and there is a security
     *         manager, the group is determined by {@linkplain
     *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
     *         If there is not a security manager or {@code
     *         SecurityManager.getThreadGroup()} returns {@code null}, the group
     *         is set to the current thread's thread group.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this thread's run method is invoked.
     *
     * @param  name
     *         the name of the new thread
     *
     * @throws  SecurityException
     *          if the current thread cannot create a thread in the specified
     *          thread group or cannot override the context class loader methods.
     */
    public Thread(ThreadGroup group, Runnable target, String name) {
        this(group, target, name, 0);
    }

修改线程的名字

不论使用的是默认的函数命名规则,还是指定了一个特殊的名字,在线程启动之前还有一个机会可以对其进行修改,一旦线程启动,名字将不再被修改,下面是 ThreadsetName 源码:

    /**
     * Changes the name of this thread to be equal to the argument {@code name}.
     * <p>
     * First the {@code checkAccess} method of this thread is called
     * with no arguments. This may result in throwing a
     * {@code SecurityException}.
     *
     * @param      name   the new name for this thread.
     * @throws     SecurityException  if the current thread cannot modify this
     *             thread.
     * @see        #getName
     * @see        #checkAccess()
     */
    public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }

线程的父子关系

Thread的所有构造函数,最终都会去调用一个内部方法init(JDK8)或者同一个构造函数java.lang.Thread#Thread(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)(JDK17)用于线程的真实创建,通过源码发现新创建的任何一个线程都会有一个父线程。

以JDK17创建线程的源码为例:

  • Thread parent = currentThread(); // 调用获取当前线程函数
  • this.daemon = parent.isDaemon();// 通过父线程守护参数设置当前线程守护线程参数
  • this.priority = parent.getPriority();// 通过父线程参数设置当前线程优先级参数

完整源码如下:

/**
     * 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
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
    @SuppressWarnings("removal")
    private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread(); // 调用获取当前线程函数
        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 manager doesn't have a strong opinion
               on the matter, use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(
                        SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();// 通过父线程守护参数设置当前线程守护线程参数
        this.priority = parent.getPriority();// 通过父线程参数设置当前线程优先级参数
        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);
        if (inheritThreadLocals && 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 */
        this.tid = nextThreadID();
    }

currentThread() 是获取当前线程,在线程生命周期中,线程的最初状态为NEW,没有执行start方法之前,它只能算是一个Thread的实例,并不意味着一个新的线程被创建,因此currentThread代表的将会是创建它的那个线程,可以得出以下结论:

  • 一个线程的创建肯定是由另一个线程完成的。
  • 被创建线程的父线程是创建它的线程。

main函数所在的线程是由JVM创建的,也就是main线程,那就意味着前面示例代码中创建的所有线程,其父线程都是 main 线程。

Thread 与 ThreadGroup

在初始化线程的构造函数有如下这段代码:

Thread parent = currentThread(); // 调用获取当前线程函数
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 manager doesn't have a strong opinion
        on the matter, use the parent thread group. */
    if (g == null) {
        g = parent.getThreadGroup();
    }
}

/* checkAccess regardless of whether or not threadgroup is
    explicitly passed in. */
g.checkAccess();

/*
    * Do we have the required permissions?
    */
if (security != null) {
    if (isCCLOverridden(getClass())) {
        security.checkPermission(
                SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
    }
}

g.addUnstarted();

this.group = g;

通过对源码分析,在创建线程时没有显示给出 ThreadGroup g,也就是 g==null这个判断为true,构造函数会隐式的给当前线程实例添加一个默认的ThreadGroup。这个ThreadGroup就是当前运行线程(或者说是父线程)的ThreadGroup

通过代码测试如下:

package engineer.concurrent.battle.onebasic;

/**
 * 测试 Thread 与 ThreadGroup
 */
public class ThreadGroupTest {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1");
        ThreadGroup group = new ThreadGroup("group1");
        Thread t2 = new Thread(group,"t2");
        ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGroup();
        System.out.println("Main thread belong group:"+ mainThreadGroup.getName());
        System.out.println("tl and main belong the same group:" + (mainThreadGroup== t1.getThreadGroup()));
        System.out.println("t2 thread group not belong main group:" + (mainThreadGroup== t2.getThreadGroup()));
        System.out.println("t2 thread group belong main TestGroup:" + (group == t2.getThreadGroup()));
    }
}

输出结果如下:

Main thread belong group:main
tl and main belong the same group:true
t2 thread group not belong main group:false
t2 thread group belong main TestGroup:true

Thread与stack size

在Thread的构造函数中有一个stackSize参数,说明内容为the desired stack size for the new thread, or zero to indicate that this parameter is to be ignored.。翻译过来的意思是stackSize指的是新线程栈的大小,可以传0代表忽略这个参数而使用默认的栈内存大小。栈内存大小这块内容是与JVM配置栈大小相关的。下面基于此分析。

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html 中提到了stackSize。具体内容如下:

The stack size is the approximate number of bytes of address space that the virtual machine is to allocate for this thread's stack. The effect of the stackSize parameter, if any, is highly platform dependent.

On some platforms, specifying a higher value for the stackSize parameter may allow a thread to achieve greater recursion depth before throwing a StackOverflowError. Similarly, specifying a lower value may allow a greater number of threads to exist concurrently without throwing an OutOfMemoryError (or other internal error). The details of the relationship between the value of the stackSize parameter and the maximum recursion depth and concurrency level are platform-dependent. On some platforms, the value of the stackSize parameter may have no effect whatsoever.

The virtual machine is free to treat the stackSize parameter as a suggestion. If the specified value is unreasonably low for the platform, the virtual machine may instead use some platform-specific minimum value; if the specified value is unreasonably high, the virtual machine may instead use some platform-specific maximum. Likewise, the virtual machine is free to round the specified value up or down as it sees fit (or to ignore it completely).

Specifying a value of zero for the stackSize parameter will cause this constructor to behave exactly like the Thread(ThreadGroup, Runnable, String) constructor.

Due to the platform-dependent nature of the behavior of this constructor, extreme care should be exercised in its use. The thread stack size necessary to perform a given computation will likely vary from one JRE implementation to another. In light of this variation, careful tuning of the stack size parameter may be required, and the tuning may need to be repeated for each JRE implementation on which an application is to run.

Implementation note: Java platform implementers are encouraged to document their implementation's behavior with respect to the stackSize parameter.

堆栈大小是虚拟机为该线程的堆栈分配的大致字节数。stackSize参数的效果在很大程度上取决于平台。

在某些平台上,为stackSize参数指定较高的值可能允许线程在引发StackOverflowError之前实现更大的递归深度。同样,指定较低的值可能允许更多的线程并发存在,而不会引发OutOfMemoryError(或其他内部错误)。stackSize参数的值与最大递归深度和并发级别之间的关系细节因平台而异。在某些平台上,stackSize参数的值可能根本不起作用。

虚拟机可以将stackSize参数视为建议。如果指定的值对于平台来说过低,虚拟机可能会使用一些特定于平台的最小值;如果指定的值过高,虚拟机可能会使用一些特定于平台的最大值。同样地,虚拟机可以自行决定是否将指定的值向上舍入或向下舍入(或完全忽略它)。

对于stackSize参数设置为0,该构造函数的行为将与Thread(ThreadGroup, Runnable, String)构造函数完全相同。

由于此构造函数的行为依赖于平台,使用时应极度谨慎。执行给定计算所需的线程堆栈大小可能因JRE实现而异。鉴于这种差异,可能需要对stackSize参数进行仔细调整,并且在应用程序要运行的每个JRE实现上可能需要重复调整。

实施注意事项:鼓励Java平台实现者记录其实现与stackSize参数相关的行为。

通过翻译总结有如下几个要点:

  • 堆栈大小是虚拟机为该线程的堆栈分配的大致字节数。
  • stackSize参数的效果在很大程度上取决于平台。
  • 执行给定计算所需的线程堆栈大小可能因JRE实现而异。
  • 在应用程序要运行的每个JRE实现上可能需要重复调整。

一般情况下stackSize参数指定较高的值可能允许线程在引发StackOverflowError之前实现更大的递归深度,指定较低的值可能允许更多的线程并发存在,而不会引发OutOfMemoryError(或其他内部错误)。

package engineer.concurrent.battle.onebasic;
import org.junit.jupiter.api.Test;

public class ThreadStackSizeTest {
    private final static int THREAD_COUNT = Integer.MAX_VALUE;
    @Test
    public void testStackSize1() {
        // 6914
        stackSizeRunMethod(10);
    }

    @Test
    public void testStackSize2() {
        // 4009
        stackSizeRunMethod(100);
    }

    @Test
    public void testStackSize3() {
        //6461
        stackSizeRunMethod(1000);
    }

    @Test
    public void testStackSize4() {
        //153
        stackSizeRunMethod(10000);
    }

    public static void stackSizeRunMethod(int stackSize) {
        ThreadGroup group = new ThreadGroup("stackSizeRunMethod");
        Runnable runnable = new Runnable() {
            public void run() {
                int i = 1;
                try {
                    recurse(i);
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                } catch (Error e) {
                    System.out.println(e.getMessage());
                }
            }
            private void recurse(int i) {
                System.out.println(i);
                if (i < THREAD_COUNT) {
                    recurse(i + 1);
                }
            }
        };
        Thread thread = new Thread(group, runnable, "ThreadStackSizeTest", stackSize);
        thread.start();
    }
}

经过测试笔者选择的Oracle OpenJDK 17,stackSize与递归深度关系不是很大,有的时候甚至成反比。

守护线程

Java中通过将线程设置为守护线程(daemon thread)来指示该线程为守护线程。守护线程在没有用户线程(也就是剩余线程均为守护线程,JVM会退出)继续运行时会自动终止。这对于执行后台任务或提供服务的线程非常有用,因为它们可以在不再需要时自动关闭。

要将线程设置为守护线程,可以使用setDaemon(true)方法。这应该在启动线程之前调用。

守护线程应该谨慎使用,因为它们可能会在程序退出时突然终止,这可能导致一些任务未能完成。

以下是一个简单的示例:

package engineer.concurrent.battle.onebasic;

public class DaemonThread {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("I am a daemon thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 将线程设置为守护线程
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(2000);
        System.out.println("I am main thread and end");
    }
}

上面例子中main主线程运行结束则守护线程thread线程也结束运行。

参考

关于作者

来自一线全栈程序员nine的八年探索与实践,持续迭代中。欢迎关注“雨林寻北”或添加个人卫星codetrend(备注技术)。

posted @ 2024-02-21 19:26  落叶微风  阅读(29)  评论(0编辑  收藏  举报  来源