java中Thread源码介绍
概述
Thread是线程的意思,在Java中我们一般都说多线程,而没有提过多进程,而且大家都知道,java中的多线程是真正的多线程,可以使用多核并行处理,而像python的多线程就是伪多线程,只能说是并发处理,那大家有没有好奇,jvm虚拟机相对于操作系统来说是一个进程,如果一个进程只能占用一个cpu,那jvm中多线程又如何实现在多核处理呢?还有操作系统对于进程状态有三个基本的定义,就绪状态、运行状态、阻塞状态,这三个状态和我们在java中说线程处于阻塞状态、就绪状态是不是一样的?本文就聊聊java中的线程,之后会分析一下Java的Thread类的源码。
线程
操作系统中引入进程是为了使多个程序并发执行,改善资源利用率,进程是操作系统中进行除处理器外的资源分配和保护的基本单位。线程是进程中能够并发执行的实体,共享进程的主存空间和资源,是处理器调度和分配的基本单位。
操作系统线程模型
要想搞明白java如何实现的多线程,要先搞明白线程的分类,线程分类如下:
- 用户线程:用户线程是在用户空间实现,线程创建、调度、同步都是都是用户自己代码实现的,操作系统只知道用户线程的进程,而该进程下的线程对内核而言是不可见的,任意给定时刻都只能有一个线程在执行,如果进程某一个线程发生阻塞,整个进程都会阻塞。
- 内核线程:由内核创建管理,一个内核线程阻塞不会影响别的内核线程,因为其是调度的基本单位,有点类似于进程,不过比进程开销小。
从上面分析可知,内核线程是由内核创建管理,看上去好像和我们自己写的程序运行没什么关系,其实不然,因为操作系统的内存管理,进程调度等都需要内核线程来完成,所以我们自己写的程序的线程要和内核中的线程对应起来。我们自己写的程序运行在用户空间,那对应的线程就是用户线程,按照上面的介绍,一个进程下的用户线程一次只能有一个运行,那这就不是并行了,这种情况的原因就是多个用户线程对应一个内核线程,然后在CPU资源分配上,只给该用户线程对应的进程分配了一个核执行,而如果要想实现真正的并行,需要使用一对一或者多对多线程模型,一对一就是说,一个用户线程对应一个内核线程,然后就可以分配到不同的CPU上运行。
jvm线程模型和操作系统线程模型对应关系
上面介绍了操作系统的线程模型,那jvm是怎么和操作系统的线程模型对应起来。
在虚拟机规范中并没有规定java要使用哪种线程模型,就目前的jdk版本来说,操作系统支持怎样的线程模型,决定了jvm中的线程是怎么映射的,由于目前windows和linux都是基于一对一的线程模型,所以java可以实现真正意义上的并行。
上面的分析回答了第一个问题,Java如何实现的并行处理,第二个问题操作系统的线程状态和jvm的线程状态的关系,这个问题的答案在下面介绍jvm中线程状态的时候回答。
java实现多线程的方式
- 实现Runnable接口:实现run方法
- 继承Thread类:由于Thread类实现了Runable接口,但run方法需要子类自己重写实现
- 实现Callable接口:由于前两种方式都无法获取多线程执行的返回值,就是run方法都是void修饰的,所以又搞了一个接口。
Thread类属性介绍
//是否是守护线程 private boolean daemon = false; //线程名称 private volatile String name; //线程优先级 private int priority; //在Thread初始化的时候,可以传入实现了Runnable的类的对象,会赋值给该变量 private Runnable target; //线程所在的组 private ThreadGroup group; //保存线程独占的局部变量 ThreadLocal.ThreadLocalMap threadLocals = null; //让父线程的inheritableThreadLocals可以传给子线程,子线程接收父线程inheritableThreadLocals的字段 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //线程id private long tid; //线程状态 private volatile int threadStatus = 0;
上面的属性中,有几个很有意思,介绍一下
守护线程
Java中的线程分为守护线程和用户线程,其两者具体定义如下
- 守护线程:为用户线程服务的线程,如垃圾回收,内存管理等线程,新建的线程最初都是用户级线程,因为默认为false,可以通过setDeamon()方法进行设置,需要在线程执行之前设置。
- 用户线程:用户执行的线程,一般通过实现Runnable接口和继承Thread实现
线程组
线程组:任意一个线程都属于某个线程组,如果没有显式的指定,所有的线程都属于默认线程组(main线程组),而且只有在线程创建的时候可以指定线程组,在执行的过程中不可以更改线程组,线程组是一个树行结构,如下:
从图中可以看出,线程组中还可以有线程组。 线程组的主要作用就是更好的管理线程,有些朋友估计有疑问,既然有线程池,为啥还要搞一个线程组,两者的目的不同,线程池是为了管理线程的生命周期,而线程组是为了方便统一管理线程。
优先级
优先级的概念就不介绍了,很好理解。是不是说高优先级的线程要比低优先级线程先执行,其实不然,高优先级只是代表了一种高概率,但是并不是严格按照这个顺序,Java中的优先级分级是从1到10,默认是5,可以通过setPriority()方法设置线程的优先级,如果线程的优先级高于线程所在的线程组中线程最高优先级,则设置无效。
threadLocals和inheritableThreadLocals
threadLocals
threadLocals这个就和我们平时使用ThreadLocal有关系了,大家仔细看,这个变量的属性是一个map,这个map中其实保存就是线程的私有变量,ThreadLocal的作用是为了多线程的时候,每个线程对变量的修改互不影响,其实就是这个变量实现的,这个map中保存的就是要修改的值,每个线程都有一个自己独立的map,然后各自修改各自的map,所以可以做到多线程隔离。
inheritableThreadLocals
这个变量的作用就是把父线程的ThreadLocal传给子线程的方法,具体的介绍之后在介绍ThreadLocal的文章中再说,这里给一个例子,大家感受一下。
private void test() { final ThreadLocal threadLocal = new InheritableThreadLocal(); threadLocal.set("宝塔镇河妖"); Thread t = new Thread() { @Override public void run() { super.run(); Log.i( "天王盖地虎," + threadLocal.get()); } }; t.start(); }
例子中,在父线程中设置了值,直接传给了子线程,可能有的胖友会说,我不用这玩意也可以把父线程的成员变量传到子线程,没错,确实可以,那种实现方式是子线程通过构造方法把父线程的成员变量给传进来的,但是那种方式是不能替代ThreadLocal的功能滴,多线程并行修改保证每个线程互不影响还是要用ThreadLocal。至于上面这个例子的底层原理是什么,下一篇在介绍ThreadLocal的文章中会介绍。
threadStatus
java线程状态,在枚举类State中都有介绍,如下
public enum State { //新建状态,线程对象新建时的状态 NEW, //就绪状态,线程调用start()方法,随时等待被CPU调度;当线程被CPU调度执行也叫RUNNABLE状态 RUNNABLE, //阻塞状态,等待synchronized锁 BLOCKED, //当调用了Object.wait(),Thread.join(),LockSupport.park()没有设置超时时间,处于当前状态 WAITING, //当调用了Thread.sleep(),Thread.join(),object.wait(),LockSupport.partUtil(),LockSupport.parkNanos(),有设置 //超时时间,挂起当前线程一段时间 TIMED_WAITING, //线程执行结束 TERMINATED }
状态之间的转换关系如下图:
现在来回答一下上面的问题,就是操作系统的状态和jvm的状态有什么关系,答案是没有关系,哈哈哈。
Thread的构造方法
public Thread( ); //传入一个实现了Runnable接口的对象,传入Thread也可以,因为Thread本身就是实现了Runnable方法 public Thread(Runnable target); //传入线程名称 public Thread(String name); public Thread(Runnable target, String name); //传入线程所在的线程组名称 public Thread(ThreadGroup group, Runnable target); public Thread(ThreadGroup group, String name); public Thread(ThreadGroup group, Runnable target, String name); //可以传入线程栈空间的大小初始化方法 public Thread(ThreadGroup group, Runnable target, String name, long stackSize);
这里面比较常用的就是第二个和第三个,如果没有传入一个实现Runnable接口的类的对象,需要子类继承当前Thread类,并重写run方法。以上所有的构造方法,都是调用init方法,如下:
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); }
进入#init方法
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
//必须设置线程名称,如果没有传默认是"Thread-" + nextThreadNum(),后面会介绍这个方法
if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) {
//如果没有传线程组,获取父线程所在的线程组 g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); //设置线程组,如果没有传,当前线程和父线程在一个线程组 this.group = g;
//判断父线程是不是守护线程,如果是,那当前线程也是 this.daemon = parent.isDaemon();
//根据父线程的优先级设置当前线程的优先级 this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
//这个就是上面介绍target时候介绍的 this.target = target;
//设置优先级 setPriority(priority);
//这段代码就是上面介绍inheritableThreadLocals代码时候举的那个例子的原理
//这里会把父线程的inheritableThreadLocals传给子线程,而且是把里面的值完全拷贝到子线程 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */
//这里就是设置线程id tid = nextThreadID(); }
分析一下上面设置线程id和线程名称的方法
线程名称
//默认Thread构造方法,线程名称构成是“Thread-”
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }
从上面的代码中可以看出,如果新建线程时没有传线程名称,则线程名称为Thread-后面接一个从0开始的数字,并且是自增,也就是说线程名称就是如下Thread-0,Thread-1这样的。
线程id
private static long threadSeqNumber; private static synchronized long nextThreadID() { return ++threadSeqNumber; }
也是一个从0开始的long类型的数字。
Thread的start方法
public synchronized void start() { //线程状态为0表示新建状态(NEW),只有处于NEW状态的才可以执行start方法 if (threadStatus != 0) throw new IllegalThreadStateException(); //将当前线程加入线程组 group.add(this); boolean started = false; try { //调用本地native方法启动线程 start0(); started = true; } finally { try { //启动失败,从线程组中移除 if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0();
上面核心的方法是调用start0()方法,这个方法是一个native本地方法,在这个方法中会调用run()方法,所以在Thread中找不到调用run方法的地方。
线程的中断
首先介绍一下操作系统中中断的概念
中断:在一个程序执行过程中,遇到紧急需要处理的事情,就让出CPU先去执行中断事件,当处理完成再回到当前正在运行的程序。常见的中断就是硬件中断,比如磁盘缓冲区满了就会发出一个硬件中断。
Java中的中断概念和操作系统的概念不一样,Java中的中断并不会停止当前线程,而只是设置了一个中断标志位,当线程发现中断标志位改变之后具体怎么处理由线程自己处理,比如可以抛出中断异常,也可以不做处理等。
举例
public synchronized void test3() { Thread thread = new Thread(){ @Override public synchronized void run() { try { while (true){ System.out.println("执行了"); //在子线程中调用了wait,当前线程处于等待状态 wait(); } } catch (Exception e) { e.printStackTrace(); } } }; thread.start(); try{Thread.sleep(520);}catch (Exception e){} //在主线程设置中断 thread.interrupt(); }
此例子可以证明像wait,sleep等可以响应中断,就是在等待过程,另一个线程觉得不想让当前线程等待了,就可以给这个线程发个中断信号,然后当前线程捕获到中断信号之后在catch中就可以做一些处理。
Thread类中和中断相关的方法
//设置中断的方法 public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } // 该方法会重置中断状态,中断的线程两次调用该方法,第一次将返回true,第二次请求该方法时将会返回false。 public static boolean interrupted() { return currentThread().isInterrupted(true); } //判断中断状态,不会重置 public boolean isInterrupted() { return isInterrupted(false); }
其他方法
//静态方法,返回当前线程引用 public static native Thread currentThread(); //静态方法,让出CPU,但是线程还是处于RUNNABLE状态,但是该线程刚让出,可能又竞争到cpu,一般在测试的时候调试多线程使用,生产环境一般不用 public static native void yield(); //判断线程是否存活 public final native boolean isAlive(); //睡眠一段时间,让出CPU,线程处于TIMED_WAITING状态。即不参与CPU竞争,即使是sleep(0)。 public static native void sleep(long millis) throws InterruptedException; //当传入0时候,无限等待,线程处于WAITING状态,当传入非0时,处于TIMED_WAITING,等待一段时间,通过wait实现 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } //参数为0 if (millis == 0) { while (isAlive()) { //无限等待 wait(0); } } else { while (isAlive()) { long delay = millis - now; //固定延迟 if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
总结
本文介绍了线程的概念,以及Java线程模型,之后分析了Java的Thread类源码。Thread类在Java中是非常重要的一个类,是实现多线程的一个基础类。
参考: