Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)

1 学习内容

  1. Thread构造函数,线程命名
  2. Thread和ThreadGroup,线程父子关系
  3. Thread和虚拟机栈,创建线程数量
  4. 守护线程概念及具体使用场景介绍

2 具体内容

2.1 Thread构造函数

Constructor Description
Thread() 分配一个新的 Thread对象。
Thread(Runnable target) 分配一个新的 Thread对象。
Thread(Runnable target, String name) 分配一个新的 Thread对象。
Thread(String name) 分配一个新的 Thread对象。
Thread(ThreadGroup group, Runnable target) 分配一个新的 Thread对象。
Thread(ThreadGroup group, Runnable target, String name) 配一个新的 Thread对象,使其具有 target作为其运行对象,具有指定的 name作为其名称,属于 group引用的线程组。
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 分配一个新的 Thread对象,以便它具有 target作为其运行对象,将指定的 name正如其名,以及属于该线程组由称作 group ,并具有指定的 堆栈大小 。
Thread(ThreadGroup group, String name) 分配一个新的 Thread对象。

线程命名

  • Thread(Runnable target, String name)

模拟银行业务办理,受理最大业务数为100个。

 package com.kangna.concurrent.chapter01;
        import java.util.concurrent.TimeUnit;
        
        public class TicketWindow implements Runnable {
        	
    	//最多受理业务数
        public static final int MAX = 50;
        private int index = 1;
    	@Override
    	public void run(){
    		while(index <= MAX){
    			System.out.println(Thread.currentThread()+" 当前处理的号码是:  " + (this.index++));
    			try{
    				TimeUnit.MICROSECONDS.sleep(100);
    			}catch(InterruptedException e){
    				e.printStackTrace();
    			}
    		}
    	}
    }

测试类

package com.kangna.concurrent.chapter01;

public class Main {
	public static void main(String args[]){
		
		final TicketWindow task = new TicketWindow();
		Thread wt1 = new Thread(task , "窗口一");
		Thread wt2 = new Thread(task , "窗口二");
		Thread wt3 = new Thread(task , "窗口三");
		Thread wt4 = new Thread(task , "窗口四");
		wt1.start();
		wt2.start();
		wt3.start();
		wt4.start();
	}

运行结果
Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)_第1张图片

2.2 Thread和ThreadGroup
在Thead的构造函数中,可以显式的指定线程的Group,也就是ThreadGroup, Thread init方法的源码:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();    // 获取当前线程作为父线程,任何新创建的线程都有一个父线程
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }
    
            /* If the security doesn't have a strong opinion of

 the matter
           use the parent thread group. 如果在Thread构造的时候没有指定一个ThreadGroup,那么子线程将会别加入到父线程所在的线程*/
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

Thread parent = currentThread();是获取当前线程,在一个线程创建之初状态为New,只是一个Thread实例,currentThread代表的是创建它的那个线程, 由此我们得出结论:

  • 一个线程的创建是由另外一个线程完成的
  • 一个线程的父线程完成了该线程的创建

示例验证

    package com.kangna.concurrent.chapter02;

    public class ThreadConstruction {
	public static void main(String args[]) {
		// 创建Thread t1,且没有将 t1 显式添加到任何Group中
		Thread t1 = new Thread();
		// 创建一个ThreadGroup
		ThreadGroup threadGroup = new ThreadGroup("TestGroup");
		// 创建Thread t2 ,并且添加到了threadGroup 中
		Thread t2 = new Thread(threadGroup, "t2");
        //获取主线程的Group
		ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGroup();
		System.out.println("主线程属于Group:" + mainThreadGroup.getName());
		System.out.println(" t1 和主线程属于同一个Group :" + (mainThreadGroup == t1.getThreadGroup()));
		System.out.println(" t2 Group不属于 主线程:" + (mainThreadGroup == t2.getThreadGroup()));
		System.out.println(" t2Group 属于 threadGroup:" + (threadGroup == t2.getThreadGroup()));
	}   
}

输出的结果为:
在这里插入图片描述
通过Thread源码和示例测试我们得出了如下结论:

  • main线程所在的ThreadGroup成为main
  • 如果构造一个线程的时候没有显式的指定ThreadGroup,程序默认它将会和父线程同属于一个ThreadGroup
    Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)_第2张图片
    总结,无论如何,线程都会被加入到某个ThreadGroup中。

2.3Thread与虚拟机栈
先简单介绍一个JVM内存。
Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)_第3张图片
JVM在执行Java程序的时候会把对应的物理内存划分到不同的内存区域,每一个区域存放不同的数据,有些分区JVM在运行时创建,有些则是在JVM启动的时候创建,JVM内存如图。

  1. 程序计数器

命令的调度最终都是由操作系统通过控制总线向CPU发送机器指令,程序计数器在JVM中的作用是存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等信息。其实,无论在任何时候,一个处理器只执行一个线程中的命令,为了能够在CPU时间片轮转切换上下文之后顺利返回到正确的执行位置(回到工作现场),每条线程都需要有一个程序计数器,各个线程之间互不影响,JVM将此块内存设为私有的。

  1. Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期同线程一样,是在JVM运行时所创建的,在线程中,方法在执行的时候都会创建一个名为栈帧(stack frame)的数据结构,主要用于存放局部变量表、操作栈、动态链接、方法出口等信息,方法的调用也对应着栈帧在虚拟机栈中的压栈和弹栈过程。

Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)_第4张图片

每一个线程在创建的时候,JVM都会为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-XSS来配置,方法的调用栈帧被压入和弹出的过程,通过图我们知道,同等的虚拟机栈如果局部变量表等占用的内存越小则可被压入的栈帧就会越多,反之则少,一般将栈帧的内存大小称为宽度,而将栈帧的数量则称为虚拟机栈的深度。

  1. 本地方法栈

Java中本地方法的接口(Java Native Interface),其实就是C/C++程序,在线程执行过程中,在线程执行的过程中,经常会碰到调用JNI方法的情况,比如网络通信、文件操作的底层,甚至是String的intern等都是JNI方法,前边提到的的线程启动等,JVM为本地方法划分的内存区域便是本地方法栈,

  1. 堆内存

堆内存是JVM中最大的一块内存区域,被所有的线程所共享,Java在运行期间所创建的所有对象几乎都存放在该内存区域,该内存区域也是GC重点照顾的区域。堆内存一般分为老年代和新生代,更细的划分为Eden区、From
Survivor区和To Survivor区,如图。

Java多线程基础(一)---深入理解Thread构造函数(Thread,ThreadGroup,JVM内存,守护线程场景)_第5张图片

  1. 方法区

方法区是被多个线程所共享的区域,它主要用于存储已被虚拟机所加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据,Java虚拟机规范中将方法区划分为堆内存的一个逻辑分区,但是它还是经常被称为“非堆”,有时也被称为“持久代”,主要是站在垃圾回收的角度进行划分,

  1. Java8元空间

Java8之后JVM的内存区域区域发生了一些改变,持久代被删除,取而代之的是元空间,元空间是堆内存的一部分,JVM为每个类加载器分配了一块内存块列表,进行线性分配,块的大小取决于类加载器的类型,sun/反射/代理对应的类加载器块会小一些,之前的版本会单独卸载回收某个类,而现在则是在GC过程中发现某个类加载器已经具备回收的条件,则会将某个类加载器相关的元空间全部回收,这样可以减少内存碎片,节省GC扫描和压缩的时间。

上面简单介绍了JVM的内存分布,其中程序计数器是比较小的一块内存,而且该内存是不会出现任何溢出异常的,与线程创建、运行、销毁等关系比较大的是虚拟机栈内存,而且栈内存划分的大小将直接决定一个JVM进程中可以创建多少个线程。
此处设计虚拟机栈内存的动态模拟,我只学习书上的结论,而没有动手操作。
示例--------------不断创建线程JVM耗尽

 public class ThreadCounter extends Thread{
	final static AtomicInteger counter = new AtomicInteger();
	public static void main(String args[]){
		try{
			while(true){
				new ThreadCounter().start();
			}
		}catch(Throwable e){
			System.out.println("failed At=>" + counter.get());
		}
	}
	public void run(){
		try{
			System.out.println("The " + counter.getAndIncrement() + " thread be created.");
			TimeUnit.MINUTES.sleep(10);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}


Java进程的内存粗略为:堆内存+线程数量 * 栈内存
其实不管是32位的还是64 的操作系统,一个进程的最大内存是有限制的,32位最大内存为2G,根据上式容易得出,线程数量和栈内存是反比关系,堆内存作为基数,线程数量越多,堆内存也会相应变小,堆内存对线程数量的影响没有像栈内存那么明显, ReserveOsMemory为系统保留内存。

  • 线程数量=(最大地址空间(MaxProcessMemory)- JVM堆内存 -
    ReserveOsMemory)/ThreadStackSize(XSS)
    当然线程数量还与操作系统有密切的关系。

2.4 守护线程

守护线程:对于守护线程我的理解就是其它线程的“保姆”,一般用于处理一些后台工作,比如:GC,在正常情况下,若JVM中没有一个非守护线程,则JVM的进程会退出,反过来说,JVM中都剩下的是守护线程了,工作线程都停了,守护的对象没有了,则JVM的进程会退出,我的理解。
也就是说守护线程具备自动结束生命周期的特性,而非守护线程不具备这个特点,如果JVM中的垃圾回收线程是非守护线程,main线程完成了工作,则JVM无法退出,因为垃圾回收线程还在正常工作。
应用场景:当你希望关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够自动关闭,此时可以考虑使用守护线程完成这样的工作。

示例:守护线程

 package com.kangna.concurrent.chapter02;
    
    import java.util.concurrent.TimeUnit;
    
    public class DaemonThread {
    	
	public static void main(String args[]) throws InterruptedException{
		//1.main线程开始
		Thread thread = new Thread(() ->
		{   
			while(true){
				try{
					System.out.println(Thread.currentThread().getName() + " mian running.");
					TimeUnit.MILLISECONDS.sleep(100);
					System.out.println(Thread.currentThread().getName() + " mian done");
				}catch(InterruptedException e){
					e.printStackTrace();
				}
			}
			
		});  //new状态
		thread.setDaemon(true);  //2. 将Thread设置为守护线程
		thread.start();  //3. 启动thread线程 ,Runnable状态
		TimeUnit.MILLISECONDS.sleep(2_0L);
		System.out.println("主线程的生命周期结束");
		//4.main
	}
}

这里最常见的一个问题就是主线程已经运行结束,而我们的子线程还没进入,还没进入running状态,CPU还没有调度。当然有时候是不能设为daemon的,让它伴随JVM销毁就可以,具体还要看场景。

到底这个守护线程有什么用尼?下面一个具体的场景

  • 需求实例:一个长连接,设置 网络中的心跳检查daemonThread(health
    check)三秒检查连接是否断了,如果断了重新连接。假如没有设置为daemon线程,我的这个应用连接已经死了, Application程序已经死掉了却关不掉,JVM不会主动关掉,因为还有一些线程在活动,发心跳不断报错,这就尴尬了, 但是如果我将这个线程设置为守护线程,连接主线程死了守护可以轻松关掉。

示例:模式将心跳检查设置为守护线程

    public class DaemonHealthCheck {
    	public static void main(String args[]) throws InterruptedException{


    
        		Thread t = new Thread(() -> {
        			Thread innerThread = new Thread(() -> { //设置一个心跳线程
        				try{
    					System.out.println("health check");
    					Thread.sleep(100_000);
    				} catch (InterruptedException e){
    					e.printStackTrace();
    				}
    			});
    			innerThread.setDaemon(true);   //设为守护线程
    		    innerThread.start();
        			try{
        				Thread.sleep(1_000);
        				System.out.println(" t thread finish done");
        			} catch (InterruptedException e){
        				e.printStackTrace();
        			}
        		});
        		t.setDaemon(true);
        		t.start();
        		TimeUnit.MILLISECONDS.sleep(5_000);
    		System.out.println(Thread.currentThread().getName());
    	}
    }

3总结

  1. Thread构造函数,线程命名;
  2. Thread和ThreadGroup,线程的父子关系;
  3. Thread和虚拟机栈,虚拟机栈与线程数量的关系;
  4. 守护线程的实际场景介绍,daemon还是很有用的。

你可能感兴趣的:(Java多线程,多线程基础学习)