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后的能够返回执行结果,后续会介绍。
对于代码世界中的不少事情,你说、我说、他说,都不如您看一眼源代码,提及来弯弯绕的东西,还不是别人写的代码?