构造Thread对象你也许不知道的几件事
先不管Thread Api有多少,先把他的构造函数搞清楚,他的构造函数里面有很多细节性的东西,譬如说他在创建的时候有ThreadGroup,如果在没有传入ThreadGroup的时候,他会不会默认传呢?如果ThreadGroup为null,会获取其他的ThreadGroup吗?ThreadGroup和Thread的关系是什么?线程和栈的关系是怎样的?
大家可能只是知道怎么用,但是里面蕴含了一些什么东西呢,可能大家未必知道了。
Thread()
:
public static void main(String[] args) {
Thread t = new Thread();
}
在创建的时候都会有一个init()的私有方法.
他主要做的事情:
- ThreadGroup g: 线程组
- Runnable target:可以理解为策略的接口
- String name:线程的名字
- long stackSize:栈的大小
他还调用了另外一个重载方法。唯一多了一个权限的东西,这个我们可以不用管。
线程的名字始终是有的,那名字的规则是什么样子的?
public static void main(String[] args) {
Thread t = new Thread();
t.start();
System.out.println(t.getName());
}
这里没有重写run()方法,里面的东西都是空的。
- 输出为:Thread-0
- 如果多创建一个会变成Thread-1
通过源码可以看到他是个静态方法,threadInitNumber默认为0,也就是说创建的时候默认起一个Thread-0的名字一直往上加。
Thread(Runnable target)
:
如果不传Runnable进去呢?
从上面图片的源码可以看出默认是为null的,为null时没有执行单元那指定什么呢?
在Thread类里面有一个Runnable target 的变量在定义的时候会传给他,因为没有传一个Runnable过去,他执行是null,那执行的时候会执行谁?
先看一下start()方法源码。
start()方法的start0会去调用run()方法。
如果传了target才会去调用run()方法(调用Runnable重写的run作为逻辑单元去执行)。
如果没有传势必要自己去重写run()方法。
public static void main(String[] args) { Thread t = new Thread() { @Override public void run () { System.out.println("我是run()方法"); } }; t.start(); System.out.println(t.getName()); }
- 否则不会执行任何东西
Thread(String name)
:
- 现在我们知道了要重写run()方法从能执行逻辑单元,并且名字为默认的名字,那怎样在构造的时候就有一个名字呢?
public static void main(String[] args) {
Thread t1 = new Thread("ThreadName");
Thread t2 = new Thread(()->{
System.out.println("Runnable...");
});
t1.start();
t2.start();
System.out.println(t1.getName());
System.out.println(t2.getName());
}
输出为: ThreadName 和 Thread-1
当Thread有自定义名称的时候,threadInitNumber会跳过,然后选择下一个自增。
Thread(Runnable target, String name)
:
传递名字的过程中也能把Runnable传递过去。
public static void main(String[] args) { Thread t = new Thread(()-> { System.out.println("Runnable..." + Thread.currentThread().getName()); }, "RunnableThread"); t.start(); }
输出为:Runnable...RunnableThread
Thread(ThreadGroup group, Runnable target,String name)
:
- 线程和线程组的关系,这一块比较重要
public static void main(String[] args) {
// Thread t = new Thread();
System.out.println(Thread.currentThread().getName());
}
创建Thread的时候不启动他,是currentThread创建的,上面的例子输出为:main。
线程启动之后呢,他的ThreadGroup是谁?先分析下源码!
也就是说如果不传ThreadGroup就会把线程父类的ThreadGroup拿出来。
public static void main(String[] args) { Thread t = new Thread("T"); t.start(); System.out.println("T线程的ThreadGroup:" + t.getThreadGroup()); System.out.println("main线程的ThreadGroup:" + Thread.currentThread().getName()); System.out.println("main线程爹的ThreadGroup:" + Thread.currentThread().getThreadGroup().getName()); }
运行结果:
可以很明显的看到ThreadGroup里面有两个线程,一个是main,一个是T,这时的ThreadGroup是他们共同的ThreadGroup。
先输出来看一下
public static void main(String[] args) { Thread t = new Thread("T"); t.start(); ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); System.out.println(threadGroup.activeCount()); }
输出结果为:3
why...输出既然有3个,那还有个什么在执行?
Thread t = new Thread("T"){ @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); Thread[] threads = new Thread[threadGroup.activeCount()]; threadGroup.enumerate(threads); Arrays.asList(threads).forEach(System.out::println);
运行结果:
原来多了一个可以监听信号(捕获事件,比如退格)的Monitor Ctrl-Break。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
:
需要注意的是:stackSize这个参数是高度依赖平台的,在某些平台上这个参数可能不会有任何作用。
stackSize是啥?先看一下java的内存结构图
栈的划分
方法区
:存放虚拟机类加载的一些信息,比如class的信息、常量、变量、静态变量等。虚拟机栈
:每一个方法被执行的时候都会创建栈帧会存放在这里,stackSize主要影响的就是这里。本地方法区
:执行一些C++的代码,比如nio、文件操作等。程序计数器
:程序执行的指令怎么交给CPU区运行,现在执行了哪个指令,比如说压栈了,然后往出弹的时候,或者执行了某个指令,都要区告诉CPU,那么下一个指令执行什么?就在这里面放着
方法区是每一个线程所共享的,虚拟机栈是每个线程单独开辟的(私有)
- 比如有10个线程,会划分10个虚拟机栈,共享的数据在虚拟机栈,数据的引用放在方法区,真正的数据在堆内存里面
栈帧结构
局部变量表
:存放方法参数和方法内部定义的局部变量。操作栈
:压栈和弹栈。动态链接
:运行时常量池中该栈帧所属方法的引用。方法出口
:方法返回地址。
程序对照代码
private int i = 0; // 放入方法区 private byte[] bytes = new byte[1024]; // 引用地址方放入方法区,具体数据在堆内存 // JVM创建一个名为“main”的线程 public static void main(String[] args) { // 开辟虚拟机栈 // 本地方法区里面可能也有 比如 start0 // 程序计数器肯定有:每一步要执行什么,发送什么指定给CPU int j = 0; // 放入局部变量表 int[] arr = new int[1024]; // 引用地址方放入部变量表,具体数据在堆内存 }
演示问题:main线程写递归操作:
- 现在我们知道虚拟机栈放的是栈帧结构,他有栈帧的宽度和深度。
- 假设栈帧的大小是固定的10兆,一个栈帧有1兆,栈的深度就为10,如果栈帧是0.5兆,栈的深度就为20,也就是说:局部变量比较多,压入的栈帧就比较少,栈帧占的内存较大,那么个数就少
运行结果:抛出StackOverflowError异常和32196。
可以简单理解为大概压了32196次栈帧在虚拟机栈里面
那怎样使用stackSize改变它呢?,很明显现在改变不了,因为main线程不是我创建的所以指定不了。
创建线程:
运行结果:抛出StackOverflowError异常和16487
改变stackSize:
运行结果:抛出StackOverflowError异常和1001158
注意:JVM栈的大小在创建的时候是不会改变的,一个线程在虚拟机栈里面占了这么多,最终也创建不了多少个线程。
栈和线程大小的关系
一个线程栈的大小给多了,那么创建线程的数是否就少了呢?如果这个数值变得 比较小,那创建线程的个数是否就比较多了呢?
以上例子执行后可能会死机,就不输出了。
线程的虚拟机栈越大创建的线程的数量可能就越小,反之越多。
如果通过Thread(ThreadGroup group, Runnable target, String name) 方式创建,没有传stackSize,那他是多少呢?
先看源码。
如果stackSize为零标识着这个参数被忽略了,他赋值给了Thread的一个类变量。源码里面没有找到他什么时候被引用,我猜想是被虚拟机去引用的。
看一看出来只有在赋值的时候才有,并没有去使用,也就是说可能是虚拟机使用native方法去使用他。
总结:
Thread()
: 创建线程对象Thread,默认有一个线程名,以Thread-开头,从0开始计数。Thread(Runnable target)
: 如果在构造Thread的时候没有传递Runnable或者没有重写Thread的run方法,该Thread将不会调用任何东西,如果传递了Runnable的实例,或者重写了Thread的run方法,则会执行该方法的逻辑单元(逻辑代码)。Thread(ThreadGroup group, String name)
:如果构造线程对象时未传入ThreadGroup,Thread会默认获取父线程的Thread作为该线程的ThreadGroup,此时子线程和父线程将会在同一个ThreadGroup中。Thread(ThreadGroup group, Runnable target, String name, long stackSize)
: 构造Thread的时候传入stackSize代表着该线程占用的stack大小,如果没有指定stackSize的大小,默认是0,0代表着会忽略该参数,该参数会被JNI参数去使用,需要注意:该参数在有一些平台有效,在有些平台则无效。