从零开始的源码分析(Thread篇)

    技术2024-05-16  75

    Thread篇

    前言Thread源码接口成员变量线程状态和优先级构造函数start()和run()exit()yield()、wait()、sleep()和join()interrupt()ThreadLocal构造方法ThreadLocalMap类set方法get方法remove() 后记

    前言

    今天约了招银的面试在10号早上,所以感觉需要加快一点进度了,接下来花两天时间搞定线程和线程池的内容,然后花一两天时间搞定锁这一块的源码,最后再把Mysql和Redis、JVM的相关内容复习一些,进行一个查漏补缺。

    Thread源码

    Thread的一般有两种实现方式(翻译自Thread类的注释):第一种,继承Thread类,第二种继承Runnable接口。 当然事实上在1.5中增加callable接口用于弥补Thread和runnable接口不能获取执行结果的问题。

    接口

    我们点开Thread类,可以发现实际上Thread类也实现了Runnable接口,那么这个Runnable接口里面有什么呢?

    public class Thread implements Runnable

    其实里面什么都没有就只有一个run的抽象方法,@FunctionalInterface是函数式接口,java1.8引入,说白了就是只包含了一个抽象方法的接口。这个注解的存在意味着我们可以使用lambda表达式来创建一个线程。 根据代码上面的注释,我们可以了解到当一个对象实现了接口Runnable的时候是用来创建一个线程的。

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

    成员变量

    Thread的成员变量非常多,而且有一些百度了一下也没找到是干嘛的,所以这边还是挑一些重要的讲一下吧,总的来说比较重要的就是ThreadLocalMap、target、priority和status。

    //线程名字 private volatile String name; //优先级 private int priority; //没注释,没百度到,也没找到引用,不知道干嘛的 private Thread threadQ; //JVM中的JavaThread指针 private long eetop; /* 是否单步执行此线程 */ private boolean single_step; /* 是否是守护线程 */ private boolean daemon = false; /* 虚拟机状态 */ private boolean stillborn = false; /* 传进来的对象 */ private Runnable target; /* 线程的组 */ private ThreadGroup group; /* 线程的上下文加载器,可以用来加载一些资源 */ private ClassLoader contextClassLoader; /* The inherited AccessControlContext of this thread */ private AccessControlContext inheritedAccessControlContext; /* 给匿名线程命名的整数,匿名线程会被命名为Thread-1这类名字,这个int会自增 */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; } /*ThreadLocal的Map用于保存变量副本,后面会提到 */ ThreadLocal.ThreadLocalMap threadLocals = null; /* InheritableThreadLocal用于子线程能够拿到父线程往ThreadLocal里设置的值 */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; /*给线程分配的栈大小*/ private long stackSize; /* * 本地线程终止之后的专用状态 */ private long nativeParkEventPointer; /* * 线程ID */ private long tid; /* 用于生成线程ID */ private static long threadSeqNumber; /* 线程状态 */ private volatile int threadStatus = 0;

    线程状态和优先级

    先说线程的优先级吧,线程的优先级并非采用的是线程状态的枚举类,而是在成员变量中设置了默认值,下限值和上限值,可以通过调用Thread.setPriority()方法来设定优先级,数值在1~10之间,如果不在这区间内会直接抛出异常,默认是5。

    /** * The minimum priority that a thread can have. */ public final static int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public final static int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public final static int MAX_PRIORITY = 10;

    在java中的线程状态一共有6种,不同于操作系统中定义,java中把运行状态和就绪状态合称为可运行状态。

    public enum State { /** * 初始状态,还没有start的线程 */ NEW, /** * 可运行状态,出于运行或者就绪状态的线程 */ RUNNABLE, /** * 线程在等待monitor锁(synchronized关键字) */ BLOCKED, /** * 无时间限制的等待,通过调用wait、join等方法进入,主要是等待其他线程进行一些 * 操作,不同于等待资源的阻塞状态。 */ WAITING, /** * 有时间限制的等待状态,通过sleep,wait(time),join(time)等方法进入 */ TIMED_WAITING, /** * 终止状态,表示线程已经完成执行。 */ TERMINATED; }

    构造函数

    Thread的构造函数一共有9个之多,但是都是调用的init方法,这里就只列出一个构造方法。在这个参数最多的构造方法中,可以指定线程的分组、任务、名字和栈的大小。

    public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } 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) { 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(); this.target = target; setPriority(priority); 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 */ tid = nextThreadID(); }

    start()和run()

    线程的start方法和run方法是两个概念,start是指这个线程可以进入运行状态,调用start之后线程会开始执行这个方法(不一定是立刻执行,要等待cpu调度),进入了runnable状态。 如果调用run的话相当于直接调用了这个target的方法,这个过程中并没有启动这个线程,还是在原来的线程上执行。

    public synchronized void start() { //判断一下线程的状态,如果不是new状态则说明已经start了,抛出异常 if (threadStatus != 0) throw new IllegalThreadStateException(); //向线程组中添加该线程 group.add(this); boolean started = false; try { //start0是一个本地方法,用于执行线程 start0(); started = true; } finally { try { //如果失败则抛出异常 if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0(); @Override public void run() { if (target != null) { target.run(); } }

    exit()

    exit()方法其实没有什么好说的,当线程完成run()方法之后就会执行这个方法,用于清空所有的内容,方便GC。

    /** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */ private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }

    yield()、wait()、sleep()和join()

    wait和sleep方法都是本地方法,前者是在object类中的方法,让出cpu的同时释放资源,而后者则是在Thread类中的本地方法,让出cpu的同时不会释放资源。

    public static native void sleep(long millis) throws InterruptedException; //在java.lang.Object中 public final native void wait(long timeout) throws InterruptedException; //也是一个本地方法,表示让其他线程先执行,不确保能够释放cpu public static native void yield();

    虽然join方法重载了多个但是最后都会调用到这个join方法上,可以看到最后其实调用的还是wait方法。

    //使用synchronized进行修饰的方法锁定了调用这个方法的对象 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"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }

    interrupt()

    调用interrupt()相当于告诉线程需要进入终止状态。 在interrupt()之前采用的是stop()方法,这个方法比较暴力,直接停止线程,容易造成一些错误。 而interrupt()则是仅仅修改了线程的标志位,希望程序员自己根据这个标志位选择何时终止线程。如果对阻塞的线程调用interrupt()方法则会抛出异常。

    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(); }

    ThreadLocal

    ThreadLocal是指线程保存的变量的副本,每个线程可以单独的操作这些变量而互不影响。

    构造方法

    ThreadLocal的构造方法比较特殊,里面什么都没有。主要还是因为ThreaLocal虽然看上去是一个存储着不同线程对象的类,实际上真正的对象都存储在线程自身的内部ThreadLocalMap中,get和set方法也是先获取线程的中map。 所以ThreadLocal实质上只是提供了map类、get、set方法,自身其实不保存任何内容。

    public ThreadLocal() { }

    ThreadLocalMap类

    ThreadLocalMap是一个内部类,和之前的HashMap有比较高的相似度。同样的是采用Entry的键值对,都是采用数组实现,如果数组中的元素超过了负载上限则需要进行扩容。 但是这个内部类并没有实现Map接口,在哈希冲突的时候采用方法也有一些差别。

    static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; // Default to 0 private void setThreshold(int len) { threshold = len * 2 / 3; } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } }

    set方法

    ThreadLocal的set方法实际上是调用的map的set方法,所以我们直接来看map的set方法。

    private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //计算哈希 int i = key.threadLocalHashCode & (len-1); //当发现哈希地址冲突的时候采用的是线性探测法 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { //如果k==null说明这个key已经被回收了 //所以进行替换,顺便清楚其他的弱引用 replaceStaleEntry(key, value, i); return; } } //清除之后判断一下长度,如果达到负载上限则需要扩容。 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

    get方法

    get方法调用的是map中的getEntry方法,相比较set方法,get方法会稍微简单一些。 简单来说就是计算一下key的哈希,然后看一下对应位置的对象是否是想要的对象。 如果不是说明可能发生了冲突,向数组的下一个地址移动,直到数组元素为空或者找到对象。

    private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //当前元素就是想要的对象,或者为空,直接返回 if (e != null && e.get() == key) return e; else //向后查找 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //清除无效的entry expungeStaleEntry(i); else //不断的向后找 i = nextIndex(i, len); e = tab[i]; } return null; }

    remove()

    remove()的原理其实和get差不多。

    /** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

    后记

    今天主要总结了一下关于Thread类的源码,通过这次总结还是有了不少收获,从面经资料上看的只是原理,但是具体的实现方法还是有很多不同的讲究。 明天需要花点力气总结下线程池的内容了,相比较线程的内容,线程池在面试中问到的概率应该会更加高一些,可以扣的细节也更加多。


    Processed: 0.011, SQL: 10