Java线程创建形式 Thread构造详解 多线程中篇(五)
Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制。
创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象。
构造方法列表
构造方法核心
如前面两个图所示,Thread共有8个构造方法
而且所有的构造方法都依赖于init方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize)
所以换一个角度思考,可以认为只有一个构造方法
这“唯一的一个构造方法”调用的是五个参数的init方法
所以说,尽管有8个构造方法,但是内部底层调用的都是init方法
这是一种编码规范与设计思维---“构造方法中不设置初始化逻辑,如果需要,那么请将初始化逻辑进行封装”
对于Thread类来说,五个参数的init方法,就是这个初始化逻辑的封装方法
所有的构造方法都依赖他。
最大集合
从init的参数方法签名来看,构造一个Thread最多需要五个值,也就是说对于一个基本的Thread,能够运行的Thread,最大集合为五个;
但是通过构造方法可以看得出来,全部都是调用的四个版本的init方法,都没有传递AccessControlContext acc,在五个参数的版本中有设置默认值。
所以目前(1.8)支持Thread运行的构造参数最大集合个数为四,他们分别是:
- ThreadGroup g
- Runnable target
- String name
- long stackSize
ThreadGroup g
ThreadGroup表示该线程所在的线程组,如果没有显式指定,那么底层调用init时,传递的参数为null
如果参数传递为null的话,ThreadGroup会有默认值的设置
如果有安全管理器,会请求管理器进行设置,如果安全管理器不存在或者根本就没有明确的指示,那么将会获取父线程的所在的线程组
父线程就是创建他的线程
Thread parent = currentThread();
所以,ThreadGroup是非必填项,如果不进行设置,会有默认初始值
Runnable target
Runnable用于封装线程任务
Runnable 是一个接口,只有一个run方法,任务的具体内容封装在run方法中
这是一个抽象方法,另外注意到在1.8中,他成为了一个函数式接口,也就是说可以使用Lambda表达式直接写Runnable
另外还需要注意到,Thread实现了Runnable接口,实现了run方法
也就是说,Thread天生自带一个任务,这个任务是什么?
他的任务就是如果target不为null,那么执行target.run(); 方法
而这个target就是通过构造方法注入进来,构造方法对内部变量target进行设置
当线程创建之后,通过start方法进入就绪状态,等待处理机的调度,一旦获得运行,线程将会执行Thread的run方法。
注意到,是Thread中的run方法,而这个run方法是调用的target.run();
所以,很显然,想要设置任务,要么继承Thread,重写run方法,此时你的任务逻辑覆盖了Thread中的逻辑,执行的是你期望的代码;
要么就是设置target,不过没有setter,只能够通过构造方法注入;
总结下:
如果不对Thread的任务进行设置,因为Thread自身就是一个Runnable,本身具备任务,只不过不设置target的话相当于是一个空方法,没什么意思,你新起一个线程,结果什么都不做,嘛意思嘛;
如果想要设置任务,重点是run方法,对run方法的设置可以通过继承Thread然后覆盖,要么就是通过构造方法设置Runnable target,只有这两种方式。
String name
每个线程,都有自己的名称,如果不进行设置,那么将会有一个默认的名字,以字符串“Thread-”开头,然后会有一个递增的序列变化
所以,对于线程名称,如果不设置对程序的正确性、效率等都不会有任何问题
long stackSize
每个线程都有私有的虚拟机栈,通过这个值可以设置栈空间的大小,内部有属性stackSize,设置的就是这个值
堆栈大小是虚拟机要为该线程堆栈分配的地址空间的近似字节数
在某些平台上,指定一个较高的 stackSize 参数值可能使线程在抛出 StackOverflowError 之前达到较大的递归深度
如果指定一个较低的值将允许较多的线程并发地存在,且不会抛出 OutOfMemoryError(或其他内部错误)
stackSize 参数(如果有)的作用具有高度的平台依赖性,某些平台这个值都可能被忽略
如果这个值设置为0表示忽略设置
所以,对于stackSize可以进行设置,如果不设置默认是0,表示忽略该参数的设置。
最小集合
综上所述,ThreadGroup g会有一个默认值通过安全管理器获得或者同父线程;String name可以设置,不设置也会自动生成一个默认值;
long stackSize依赖平台严重,不建议设置,默认指定为0,表示忽略参数的设置;
对于Runnable target,如果不进行设置,也会存在一个默认的run方法,但是相当于空方法,毫无价值,所以你必须想办法将任务进行设置。
所以,一个线程运行初始化设置的最小集合为“任务封装”,到底是覆盖run方法还是传入Runnable对象?您看情况来
多线程的存在就是为了执行任务的,所以,如果想让一个线程有意义,Runnable target是必须存在的
Runnable target 是一个线程存在的必要条件,否则没有意义,所以必须设置(尽管你不设计对运行上来说不会出错)
start方法与run方法
我们已经很明确的说明,run方法来自于Runnable接口,用于封装需要执行的任务。
Thread是一个类,继承了Runnable接口,Thread类可以被实例化,Thread实现了run方法,所以Thread有一个run方法
所以,你看,run方法就是一个普通的方法
单纯的看待run方法,Thread就是一个普通的类,Runnable就是一个普通的接口,他有一个抽象方法run,Thread实现了他
如果运行run方法,就跟平时调用一个对象的方法没什么区别,所以run方法的调用跟多线程没有半毛钱关系,你就是调用一个对象的一个方法而已
所有的一切,都还是在主线程中执行,没有产生任何新的线程。
对于start方法,代码如下
start方法的调用,将会使使该线程开始执行,Java 虚拟机将会调用该线程的 run 方法,接着就是线程并发的运行了
可以看得出来,start方法并没有调用run方法,关键是在于start0,这是一个native方法,依赖于本地方法,毕竟你JVM再牛逼还是要调用底层,有本事你自己跑起来一个看看?
对run方法的执行也是在start0中触发的,如果start0正确执行,没有抛出异常,将会设置标志位started=true;
另外,一个线程如果一旦启动,再次启动时会抛出异常
看得到threadStatus用于标志是否还没有启动,如果不等于0,说明已经启动了。
所以说,说到这里,start和run方法有什么区别?
他们就没什么相似的地方,start用于线程的初始化,并且调度执行run方法封装的任务,run却仅仅是封装了任务代码的一个普通方法。
一个封装了线程的初始化逻辑,一个只是单纯的任务封装。
对于Thread中start方法和run方法的设计理念,是不是模板方法模式的应用?模板方法模式的意图如下:
定义一个操作中的算法的骨架,而将一些步骤延时到子类中
TemplateMethod使得子类可以不改变一个算法的结构即可重新定义算法的某些特定步骤
所有的线程初始化的逻辑是相同的,但是每个线程需要执行的任务是千差万别的;
在Thread中,start方法构建了初始化的逻辑,而将具体的行为转移到run方法中。
创建线程
随便百度一下“java创建线程方式”会出来一大堆文章,有说三种方式,也有的说四种方式(线程池也算一种?)
本人不能说人家的就是错的,但是至少是不准确的。
前面已经提到过,Thread是Java语言本身对线程的抽象,也就是说在Java中,线程只有一种形式,那就是Thread的实例形式存在。
如何创建一个Thread的实例对象?
只有一个途径,那就是借助于new
对于线程任务的执行,new Thread之后,调用start方法,会调用Thread的run方法,Thread自带一个run方法,实现自Runnable接口
所以,对于线程任务的设置,换一个问题就是:“如何改变这个run方法为你需要的任务代码?”
所以,你可以继承Thread,重写run方法,完全替代掉这个方法
Thread的子类仍旧是Thread,通过重写run方法后将线程任务进行封装,也就是有了任务代码,new Thread子类().start()即可。
另外,这个方法很显然,并没有做什么,只是透传到target的run方法,所以如果对target进行设置,也可以达到效果
所以准确的说,设置run方法,封装任务代码的途径有两种
- 继承Thread,重写run方法;
- 通过构造方法传递Runnable;
第一种途径--继承Thread,重写run方法
第二种途径--使用Runnable实例对象构造
对于网上的有些示例,比如下面所示,从第一行“MyThread implements Runnable”就会给人误导,他明明是一个Runnable,你起个名字MyThread?????线程???
反正多年前刚刚接触时我还以为Thread是Runnable(从实现上来说Thread是Runnable类型,但是实现接口是为了run方法,逻辑上来说,线程是线程,线程任务是线程任务)
创建一个线程,跟Runnable没半毛钱关系,任务封装才跟Runnable息息相关。
public class MyThread implements Runnable {//实现Runnable接口 public void run(){ //重写run方法 } } public class Main { public static void main(String[] args){ //创建并启动线程 MyThread myThread=new MyThread(); Thread thread=new Thread(myThread); thread().start(); //或者 new Thread(new MyThread2()).start(); } }
还有一种形式是实现Callable接口,并且借助于FutureTask
其实这根本上也是一种使用Runnable实例构造的另外一种形式,我们分析下这个过程
Callable是一个接口(1.8后函数式接口),包含一个call方法,里面可以用来封装线程执行任务,不同于run方法,可以用有返回值(具体细节后续会详细说明)
而FutureTask是个什么东西?看下图,很显然,他就是一个Runnable
内部持有一个Callable,可以通过构造方法进行注入
FutureTask futureTask = new FutureTask(myCallable);
FutureTask既然是一个Runnable,自然可以传递给Thread,用于任务的封装,我们具体看下FutureTask的run方法
细节不看,你会发现run( )方法的调用绕来绕去到了内部的callable的call( )方法调用
所以可以说:
创建Thread实例,有一种途径,那就是通过new ,借助于构造方法创建一个Thread类型的对象;
而对于任务的封装,有两种方式,一种是继承Thread,重写run方法;另外一种是实现Runnable接口,将实例对象传递给Thread用来构造;
而对于借助于Runnable实例对象的方式,又有两种形式,借助于Runnable接口或者Callable和FutureTask接口
总结
本文对Thread的构造方法进行了详细的介绍,尽管构造方法个数很多,但是逻辑很清晰
造方法借助于底层的init方法完成初始化的封装逻辑
这是一种优秀的规范--构造方法中不涉及初始化逻辑,如果需要可以进行封装。
对于构造方法中用到的各种属性进行了介绍,列出来了构建一个Thread的最大属性集合以及最小属性集合。
start方法和run方法本身有天壤之别,但是对于新人或许却容易混淆,其实多了解一下源代码以及API文档就好了
start和run方法运用了模板方法模式,也是一种很好地编程思维,将不变和变化进行解耦
对于Thread的创建,只有一种方式,那就是new对象(一家之言)
但是对于任务的封装,却有两种方式,之所以这两种方式,是由run方法的实现决定的,run就代表任务,任务就是run方法,所以就只能替换掉run,但是你又会发现他是个空方法除非target不为null,所以我们又可以替换掉target,所以就有了这两种方式
对于第二种方式,又有两种形式直接实现Runnable封装任务或者通过Callable和FutureTask配合,后者是1.5后的可以返回执行结果,后续会介绍。
对于代码世界中的很多事情,你说、我说、他说,都不如您看一眼源代码,说起来弯弯绕的东西,还不是别人写的代码?