多线程,Thread构造方法源码简介

本文详细分析了Java中Thread类的构造方法,包括线程名的生成、线程组的处理、安全检查、线程的启动逻辑等。通过源码解读,解释了为何创建线程时需要传入Runnable或覆写run()方法,并强调了start()方法的重要性。
摘要由CSDN通过智能技术生成

Java中Thread类是构造线程的一个关键类,正确创建一个线程,意味着如何构造一个正确的线程对象,因此对线程对象的构造就不得不对Thread类构造函数源码进行分析,以下是本人的一些拙见,若有不对之处,望各位大佬勿喷!

首先我们来看一下Thread类源码中的构造函数:

乍一看有8个被public修饰的和1个被default修饰的构造函数,一些javaer不禁感叹这我哪儿记得住啊!不要急,我们从源码中可以看到每个构造函数调用了一个init方法,所以我们了解Thread构造方法的根源就查看init方法是如何来实现的就行了,看到这儿是不是就开心了许多,接下来我们通过源码来具体看看,init方法具体是如何来实现创建一个Thread对象的。

在这里我简单的将init方法分为以上9个步骤,然后分别通过分析传入的6个参数进行源码分析

首先来看第一步,name也就是线程名字,当调用init方法时,他会首先判断name是否传入为空,当为空的时候他会报空指针异常,但是如果我们采用第一个Thread构造方法的时候,他并没有传入任何参数,为什么他不会报错呢,原因在于当未传入任何参数时调用init方法,它会调用nextThreadNum()方法自动为其生成一个线程名,从源码中看到这个自动生成的线程名是Thread-xx的格式,并且nextThreadNum()是被synchronized关键字修饰的,所以是线程安全的,不会引发多个线程生成线程名字混乱的情况。当然了,如果我们自己传入一个线程名,那么我们创建的线程名就是给定的名字。

总结一下第一步:当用户传入线程名时,当前创建的线程就是给定的名字,如果用户不单独传入,则在init()方法中会调用nextThreadNum()方法自动为其生成一个格式为Thread-xx的线程名,并且nextThreadNum()方法被synchronized修饰,生成的线程名是线程安全的。这样保证了调用init()方法时,不会发生线程名为空而引发空指针异常的情况。

接下来看看第二步:进入第二步会首先通过你调用currentThread()方法获取创建当前线程的父线程,通过源码不难发现,该方法是被native修饰的,底层通过c++实现,这里就不做过多介绍,我们只需知道,通过该方法获取当前线程的父线程即可。接下来会获取一个安全管理器对象,但是这些都不是主要的,我们重点来看一下传入的线程组参数g,他会首先判断是否传入了线程组参数g,如果为空会继续通过刚才的安全管理器对象去获取线程组,最后如果还是没有线程组对象,他会默认创建该线程的父线程其归属的线程组,即子线程和父线程在同一个线程组中。

以下做一个简单的例子,便于大家理解,当未传入ThreadGroup对象时,当前创建该线程的父线程是main,,当前线程组就是当前创建的线程和父线程main,当然这里说的不全对,因为还存在一些守护线程,这里不做过多的介绍。

总结一下第二步:当未传入ThreadGroup对象时,会首先在安全管理器中去获取当前线程组,如果没有获取到,会将创建该线程的父线程归属到和子线程相同的线程组中。

第三四步是做一下安全检查,不必去深究。

接下来看一下第五步:

第五步,线程组g调用的addUnstarted()方法,在源码中注释到其作用是增加线程组中未启动线程的计数。未启动的线程不会添加到线程组中,这样如果它们从未启动过,就可以收集它们,但是必须对它们进行计数,以便包含未启动线程的守护线程组不会被销毁。接下来就是获取线程组属性,isDaemon()判断是否是守护进程,getPriority()获取线程的优先级。

第六步还是做一些安全的检查,第七步在源码中判断acc时调用的AccessController.getContext()的方法是说当前线程继承的AccessControlContext和任何有限的权限范围,会将其放置在AccessControlContext对象中。然后可以在以后的某个线程中检查该上下文。

第八步中target是一个Runnable ,Runnable 接口中只有一个run方法,具体当前线程进行什么逻辑操作都是需要根据我们自己的需要实现Runnable 接口进行覆写run()方法的。回到源码中来,Thread是实现了Runnable接口,在调用run()方法时会判断当前传入的参数target是否是空,如果是空或者没有覆写Thread中run()方法,该线程将不会做出任何反应,如果传递了Runnable接口的实例,或者覆写了run()方法,则会执行覆写的run()方法的逻辑单元。

通过第八步,在这里说一下run()方法和start()方法之间的关系,很多朋友会问,为什么调用了run()方法还要调用start()方法呢?因为从源码中不难发现,Thread是一个类,它继承了Runnable 接口,当实例化Thead后,它实现了run(),所以run()方法仅仅只是一个被覆写了的普通方法,并没有任何特殊之处,如果没有调用start(),run()方法只是在主线程中进行,没有创建任何新线程。

看下面的start()方法源码,调用start()方法后,JVM会调用创建线程的run()方法,在源码中没有体现,是通过底层调用本地方法start0()进行实现的,并且同一个线程不能被同时start()两次,否则会报IllegalThreadStateException异常。

总结一下第八步:当我们创建线程时一定要传递Runnable接口的实例或者覆写run()方法,如果什么都没操作,该线程不会进行任何操作。创建一个线程需要调用start()方法才能真正的使得该线程起作用,如果是单核cup的话,一旦启动start()方法,该线程并不能保证会立即执行,只是处于一个可执行状态,因为,当前线程需要和其他线程抢夺时间片,当获得时间片后,他才会真正的执行。

第九步就会涉及到JVM一些知识,stackSize指的是当前线程所需要的堆栈的大小,如果没有指定会默认忽略此参数。方法介绍中还指出,该参数在一些平台是有效的,一些平台则无效。最后就是获取线程的一个id,源码中也可以看到该方法也是被synchronized修饰的,因此获取到的线程id是线程安全的。

说到这里,创建一个简单的线程就基本搞定了!主要的就是明白Thread类构造方法的参数是什么含义,具有什么作用,能够帮助我们更加深刻的理解一个线程的创建思路。

最后,鉴于本人第一次写一些自己的认识,希望对有需要的朋友有所帮助,如若有不正确的地方,还望各位大佬在评论区留言,我将会不断地去提高自己,尽最大努力写出一些更好的文章。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值