JAVA中多线程的操作对于初学者而言是比较难理解的,其实联想到底层操作系统时我们可能会稍微明白些,对于程序而言最终都是硬件上运行二进制指令,然而,这些又太过底层,今天来看一下JAVA中的线程,浅析JDK源码中的Thread类,之后能帮助我们更好的处理线程问题

前言

Thread思维导图

JDK版本号:1.8.0_171

在Thread注释中可以看到大佬对其进行的解释:

Thread就是程序中一个线程的执行.JVM允许一个应用中多个线程并发执行

每个线程都有优先级.高优先级线程优先于低优先级线程执行
每个线程都可以(不可以)被标记为守护线程
当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样
当且仅当父线程为守护线程时,新创建的线程才会是守护线程

当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)
JVM会持续执行线程直到下面某一个情况发生为止:
1.类运行时exit()方法被调用且安全机制允许此exit()方法的调用.
2.所有非守护类型的线程均已经终止,或者run()方法调用返回或者在run()方法外部抛出了一些可传播性的异常.

可以联想下JVM的启动过程,从main方法启动,可以自己写代码查看下线程情况

线程实现

Thread注释类上清楚的写明了线程的两种实现方式:

  • 定义一个继承Thread类的子类,子类可覆写父类的run()方法
  • 实现Runnable接口

示例如下,相信各位读者应该非常熟悉了

    public static void main(String[] args) {
        // 1.继承Thread类
        ExtendsThread extendsThread = new ExtendsThread("test1");
        extendsThread.start();
        // 2.实现Runnable接口
        ImplementsRunnable implementsRunnable = new ImplementsRunnable("test2");
        // 实现Runnable接口的类不能单独使用,最终还是要依赖Thread
        Thread implementsRunnableThread = new Thread(implementsRunnable);
        implementsRunnableThread.start();
    }

    static class ExtendsThread extends Thread{

        private String name;

        public ExtendsThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("ExtendsThread is " + name);
        }
    }

    static class ImplementsRunnable implements Runnable{

        private String name;

        public ImplementsRunnable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("ImplementsRunnable is " + name);
        }
    }

从上面的实现中我们能看到最终都需要实现Runnable接口,也就是实现run方法,Thread本身也是实现了Runnable接口(可参考源码部分)

Runnable

先看下Runnable接口实现:

public interface Runnable {
    public abstract void run();
}

非常简单,就一个run方法,也就意味着我们只要将自定义代码逻辑在run方法中实现即可,我们在平常的线程实现中最简单的使用方式也就是如上面线程实现的方式,所以我们最终需要了解的部分还是在于Thread,接下来就看看本文的重点Thread

类定义

public class Thread implements Runnable

Thread关系图

初始化

先了解下static块的部分,这里在类首次加载时执行,主要作用就是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦

通俗点说就是我们在之后调用native方法时,jvm会直接去调用本地系统方法完成操作,不会再去查找这个方法在哪,再去链接等等,相当于先进行注册,之后就直接操作使用,因为涉及到底层c语言,这里不过多深入解释

    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

内部接口/类

Thread内部实现了一些接口和类,下面来一一进行说明

UncaughtExceptionHandler

未捕获异常处理器,在线程由于未捕获的异常终止时,JVM会进行一些处理,处理流程如下:

  • JVM调用终止线程的getUncaughtExceptionHandler方法获取终止线程的uncaughtExceptionHandler
  • 非null则调用uncaughtExceptionHandler的uncaughtException方法,同时将此终止线程和其异常作为参数传入
  • null则找到终止线程所在的最上级线程组,调用其uncaughtException方法,同时将此终止线程和其异常作为参数传入
  • 调用Thread.getDefaultUncaughtExceptionHandler获取handle,非空则调用其uncaughtException方法,空则判断调用e.printStackTrace(System.err)处理

整个调用处理过程可参考下列源码部分:

    // 内部异常处理接口
    public interface UncaughtExceptionHandler {

        void uncaughtException(Thread t, Throwable e);
    }
    
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        // 返回handle本身或线程组(实现了这个接口)
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
    
    // ThreadGroup中代码逻辑
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                // 默认handle非空则调用
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                // 未设置基本都是这里进行打印堆栈信息
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

可参考下列测试代码理解:

    public static void main(String[] args) {
        // 实现UncaughtExceptionHandler接口
        Thread.UncaughtExceptionHandler handler = (t, e) -> {
            System.out.println("test:" + t.getName());
            e.printStackTrace();
        };
        // 设置默认UncaughtExceptionHandler
        // Thread.setDefaultUncaughtExceptionHandler(handler);
        ExtendsThread extendsThread = new ExtendsThread("test");
        // 设置UncaughtExceptionHandler
        extendsThread.setUncaughtExceptionHandler(handler);
        extendsThread.start();
    }

    static class ExtendsThread extends Thread {

        private String name;

        public ExtendsThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            int a = 1 / 0;
            System.out.println(a);
        }
    }

从这个接口我们可以知道对于线程异常停止我们其实是可以进行一些后续操作的,通过实现UncaughtExceptionHandler接口来进行线程异常后续处理操作,不过我还没见过有什么源码中用过,一般都是直接catch中处理,这里就先了解下即可

Caches/WeakClassKey

我们可以看到WeakClassKey这个内部类继承了WeakReference,而WeakClassKey被Caches所使用,从名字我们也能明白其部分含义,本地缓存,WeakClassKey是弱引用相关类,至于弱引用的使用大家可以自行Google,这里不多说,如果你看过《深入理解Java虚拟机》,应该多少有点了解

subclassAudits提供了一个哈希表缓存,该缓存的键类型为java.lang.Thread.WeakClassKey,注意看它的值类型是一个java.lang.Boolean类型的,从其代码注释可以知道这个哈希表缓存中保存的是所有子类的代码执行安全性检测结果
  
subclassAuditsQueue定义了一个Queue队列,保存已经审核过的子类弱引用

    /** cache of subclass security audit results */
    /* Replace with ConcurrentReferenceHashMap when/if it appears in a future
     * release */
    private static class Caches {
        /** cache of subclass security audit results */
        // 缓存安全检查结果
        static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits =
            new ConcurrentHashMap<>();
        // 队列
        /** queue for WeakReferences to audited subclasses */
        static final ReferenceQueue<Class<?>> subclassAuditsQueue =
            new ReferenceQueue<>();
    }

    /**
     *  Weak key for Class objects.
     **/
    static class WeakClassKey extends WeakReference<Class<?>> {
        /**
         * saved value of the referent's identity hash code, to maintain
         * a consistent hash code after the referent has been cleared
         */
        private final int hash;

        /**
         * Create a new WeakClassKey to the given object, registered
         * with a queue.
         */
        WeakClassKey(Class<?> cl, ReferenceQueue<Class<?>> refQueue) {
            super(cl, refQueue);
            hash = System.identityHashCode(cl);
        }

        /**
         * Returns the identity hash code of the original referent.
         */
        @Override
        public int hashCode() {
            return hash;
        }

        /**
         * Returns true if the given object is this identical
         * WeakClassKey instance, or, if this object's referent has not
         * been cleared, if the given object is another WeakClassKey
         * instance with the identical non-null referent as this one.
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof WeakClassKey) {
                Object referent = get();
                return (referent != null) &&
                       (referent == ((WeakClassKey) obj).get());
            } else {
                return false;
            }
        }
    }

在源码中我们可以看到主要在isCCLOverridden中使用到了,这里直接看下这个方法的源码,注释上也写明了,验证实例(可能是子类)没有违反安全约束可以被构建,子类不能重写安全相关的非final方法,否则需要检查enableContextClassLoaderOverride运行权,主要是进行安全检查的,简单理解就好

    /**
     * Verifies that this (possibly subclass) instance can be constructed
     * without violating security constraints: the subclass must not override
     * security-sensitive non-final methods, or else the
     * "enableContextClassLoaderOverride" RuntimePermission is checked.
     */
    private static boolean isCCLOverridden(Class<?> cl) {
        if (cl == Thread.class)
            return false;

        processQueue(Caches.subclassAuditsQueue, Caches.subclassAudits);
        // 生成key
        WeakClassKey key = new WeakClassKey(cl, Caches.subclassAuditsQueue);
        // 从缓存查找
        Boolean result = Caches.subclassAudits.get(key);
        if (result == null) {
            result = Boolean.valueOf(auditSubclass(cl));
            Caches.subclassAudits.putIfAbsent(key, result);
        }
        // 返回结果
        return result.booleanValue();
    }

State

线程状态枚举类,表明线程处于生命周期中的哪个阶段,线程在任意时刻只会处于下列其中一种状态,只反映JVM中的线程状态,不是反映操作系统线程状态

    public enum State {
        /**
         * 初始态
         * 线程创建完毕还未启动,未调用start方法
         */
        NEW,

        /**
         * 包含两种状态
         * 1.就绪态
         * 2.运行态
         */
        RUNNABLE,

        /**
         * 阻塞态
         * synchronized会导致线程进入blocked状态
         */
        BLOCKED,

        /**
         * 
         * 等待态
         * 3种情况调用后会导致线程处于这个状态:
         * 1.Object.wait
         * 2.Thread.join
         * 3.LockSupport.park
         * 
         * 等待态的线程会等待其他线程执行特定的操作
         * 
         * 例如一个线程调用了Object.wait之后进入等待态,另一个线程调用Object.notify或Object.notifyAll可以将其唤醒,被唤醒的线程需要获取对象的锁才能恢复执行
         * 调用Thread.join会等待指定的线程终止
         */
        WAITING,

        /**
         * 
         * 超时等待态
         * 线程等待指定的时间再执行
         * 5种情况调用后会导致线程处于这个状态:
         * 1.Thread.sleep
         * 2.Object.wait 传入等待时长
         * 3.Thread.join 传入等待时长
         * 4.LockSupport.parkNanos
         * 5.LockSupport.parkUntil
         */
        TIMED_WAITING,

        /**
         * 终止态
         * 线程执行完毕
         */
        TERMINATED;
    }

线程状态转化

线程状态转化可参考下图理解

线程状态转化图

在Java中线程分为6种状态,上面枚举类也已经说明了,这里稍微详细点说明下每种状态的含义:

初始态(NEW)

  • 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态

运行态(RUNNABLE)

运行态在Java中包括就绪态和运行态

1.就绪态:

  • 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行
  • 所有就绪态的线程存放在就绪队列中

2.运行态:

  • 获得CPU执行权,正在执行的线程
  • 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一个运行态的线程

阻塞态(BLOCKED)

  • 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态
  • 而在Java中,阻塞态专指请求锁失败时进入的状态
  • 由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行

等待态(WAITING)

  • 当前线程中调用wait、join、park函数时,当前线程就会进入等待态
  • 也有一个等待队列存放所有等待态的线程
  • 线程处于等待态表示它需要等待其他线程的指示才能继续运行
  • 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)

超时等待态(TIMED_WAITING)

  • 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态
  • 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后被其他线程唤醒或超时自动唤醒
  • 进入该状态后释放CPU执行权和占有的资源,其中wait()方法会释放CPU执行权和占有的锁,sleep(long)方法仅释放CPU使用权,锁仍然占用
  • 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁

终止态(TERMINATED)

  • 线程执行结束后的状态

其中有几点需要注意的:

  • yield方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行
  • wait和notify必须配套使用,即必须使用同一把锁调用
  • wait和notify必须放在一个同步块中调用,wait和notify的对象必须是他们所处同步块的锁对象

总结

以上对Thread进行了简单的介绍说明,对于Thread的状态转换需要多理解理解,写些代码debug或者通过jdk工具观察下jvm的线程状态还是很有必要的

以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢

参考


orange
17 声望5 粉丝