欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。
Thread类是一个构建线程的关键类,通过传递一个实现了Runnable接口的类就可以简单构造出一个线程对象,下面就来看看有关Thread类的一些基础知识点吧(本文略长请耐心阅读,相信你一定受益匪浅)。
Thread一共有8种(public修饰)构造函数和一种(default修饰)默认构造函数,分别如下所示:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {
init(group, target, name, stackSize);
}
通过观察以上的构造函数,其实不难发现,所有的构造方法最终都是调用了一个叫"init()"的方法。接下来我们就重点来关注下这个“init()”方法到底做了一些什么事,完成了哪些操作。
通过观察源码可以发现“init()”方法也有两个重载,但是最终的调用还是参数比较多的那个方法。其实根据多阅读源码的经验,不难发现,把参数最多的一个方法封装好,可以根据方法的重载来实现不同参数的方法实现。然后我们重点来解读参数较多的“init()”方法。
private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(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) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(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);
}
this.stackSize = stackSize;
tid = nextThreadID();
}
乍一看上面的“init()”里面的内容确实有点多,请不要着急,静下心来跟着我的思路一点一点的阅读。
if (name == null) {
throw new NullPointerException("name cannot be null");
}
第一步,就是对线程名字进行一个判断,判断是不是等于空,如果传递的线程名字是空值的话,那就会抛出空指针异常来提醒用户。也许你一时间会诧异,为什么之前构造的线程没有传递名称,他也能正常运行呢?比如下面这样:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("没有构造线程名称");
}
}
public class MyRunnableTest {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
不错,这样是可以正确运行的,也是能够输出“没有构造线程名称”这句话的。其实这个还要回到文章刚开始的地方,8个公共的构造方法的重载上。通过跟进上述例子中的源码不难发现,上述例子中走的构造方法是下面的这个:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
也许细心的你已经发现了,构造方法中会默认帮我们生成一个以"Thread-"开头整数结尾的线程名。因为“nextThreadNum()”使用了“synchronized”关键字修饰,所以他是线程安全的,不必担心多个线程会引起数字错乱的问题。
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
第二步,就是给线程赋予名字,这个名字如果你指定了一个名字,那线程名就是你指定的。如果没有指定,那就是构造函数给你默认生成的一个。
我们都应该知道一个线程的构造和启动,肯定是由另一个线程来完成的。因此下面这个代码就是获取构造这个线程的父线程:
Thread parent = currentThread();
而“currentThread()”方法是一个本地方法,也就是交给底层去操作了。我们无须关系它到底实现了什么,只需要先知道他为我们拿到了构造这个线程的父线程就行了。
public static native Thread currentThread();
第三步,判断我们传递的“ThreadGroup”是不是为空,目的就是显示的为线程对象设置一个线程组,如果没有显示的设置线程组(也就是“g == null”条件成立)则会去SecurityManager获得线程组(SecurityManager具体含义后面文章再讲)。如果还是没有获取到则设置为父线程所归属的线程组。
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
第四步,就是做一些安全检查,这里不太重要,也不必要过于纠结。
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(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;
}
第八步,是设置该线程的继承的AccessControlContext(AccessControlContext用于根据其封装的上下文做出系统资源访问决策,具体的介绍可以查看API的介绍)。
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
第九步,是设置与此线程有关的InheritableThreadLocal值,它由一个ThreadLocalMap保存(后期文章讲ThreadLocalMap会着重讲到它,这里可以简单理解为一个map)。
if (inheritThreadLocals && parent.inheritableThreadLocals != null){
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
第十步,是设置新线程的所需堆栈大小,如果你了解JVM Stack(虚拟机栈)那么这个栈的深度应该就很容易理解。如果不了解的话,就认为它开辟了一块内存空间即可。
this.stackSize = stackSize;
第十一步,是设置线程的ID,每一个线程都有一个唯一的ID进行标识。可以看到线程ID的设置是调用了“nextThreadID()”而“nextThreadID()”也用了“synchronized ”关键字进行修饰,所以也是线程安全的
tid = nextThreadID()
private static long threadSeqNumber;
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
给一个图希望加深印象(画图写作真的很累,希望能帮到你,顺便公众号关注我下哈*_*):
到此为止,一个基本的线程对象就构建完成了,不过需要注意的是他还没有启动,只是简单的构造了一个线程对象。