构造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参数去使用,需要注意:该参数在有一些平台有效,在有些平台则无效。

results matching ""

    No results matching ""