Thread源码阅读及相关问题

1,167 阅读7分钟

前言

《java线程池源码阅读》发出后很多小伙伴问到了Thread相关的问题。因为Thread是并发的基础,所以今天我们围绕一些问题来对Thread进行源码级别的了解。

除了小伙伴问的问题,笔者还整理了一下,涵盖以下问题:

  • run()方法和start()方法的区别?
  • 异步线程想要返回结果咋办?
  • 线程死锁啥时候发生的?
  • 线程的生命周期是怎样的?
  • sleep、join、wait的区别?
  • interrupt()stop的区别
  • 线程假死啥时候发生,怎么排查?

下面我们还是先对Thread做一个基本了解。

Thread

关于线程的基础知识,可以看下笔者之前的文章《操作系统之进程管理》

线程是程序中的执行线程。JVM允许应用程序同时运行多个执行线程。

每个线程都有优先级。优先级较高的线程优先于优先级较低的线程执行。每个线程也可以标记为守护线程,也可以不标记为守护线程。当在某个线程中运行的代码创建新的线程对象时,新线程的优先级最初设置为与创建线程的优先级相等,并且仅当创建线程是守护线程时,该线程才是守护线程。

当JVM时,通常只有一个非守护进程线程(它通常调用某个指定类的main方法)。JVM将继续执行线程,直到发生以下任一情况:

  • 调用Runtime类的exit方法并且安全管理器已允许执行exit操作。
  • 所有非守护线程都已死亡,要么是通过调用run方法返回,要么是通过抛出传播到run方法之外的异常。

Tomcat Web应用服务器为啥能一直运行呢?

根据前面的非守护线程的介绍,很让人能想到服务为啥能一直运行,原因是其中有的线程在做“无限”循环,这个循环如果你不手动结束它它就永远不会结束,因此Tomcat能够一直在后台运行。

我们可以跟踪Tomcat的Bootstrap的main方法。你会发现最终有一个异步方法中有一个while循环:

public final class StandardServer extends LifecycleMBeanBase implements Server {
        /**
     * Wait until a proper shutdown command is received, then return.
     * This keeps the main thread alive - the thread pool listening for http
     * connections is daemon threads.
     */
    @Override
    public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        if (getPortWithOffset() == -2) {
            // undocumented yet - for embedding apps that are around, alive.
            return;
        }
        if (getPortWithOffset() == -1) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }

        // Set up a server socket to wait on
        try {
            awaitSocket = new ServerSocket(getPortWithOffset(), 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            log.error(sm.getString("standardServer.awaitSocket.fail", address,
                    String.valueOf(getPortWithOffset()), String.valueOf(getPort()),
                    String.valueOf(getPortOffset())), e);
            return;
        }

        try {
            awaitThread = Thread.currentThread();

            // Loop waiting for a connection and a valid command
            while (!stopAwait) {
                ServerSocket serverSocket = awaitSocket;
                if (serverSocket == null) {
                    break;
                }

                // Wait for the next connection
                Socket socket = null;
                StringBuilder command = new StringBuilder();
                try {
                    InputStream stream;
                    long acceptStartTime = System.currentTimeMillis();
                    try {
                        socket = serverSocket.accept();
                        socket.setSoTimeout(10 * 1000);  // Ten seconds
                        stream = socket.getInputStream();
                    } catch (SocketTimeoutException ste) {
                        // This should never happen but bug 56684 suggests that
                        // it does.
                        log.warn(sm.getString("standardServer.accept.timeout",
                                Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                        continue;
                    } catch (AccessControlException ace) {
                        log.warn(sm.getString("standardServer.accept.security"), ace);
                        continue;
                    } catch (IOException e) {
                        if (stopAwait) {
                            // Wait was aborted with socket.close()
                            break;
                        }
                        log.error(sm.getString("standardServer.accept.error"), e);
                        break;
                    }

                    // Read a set of characters from the socket
                    int expected = 1024; // Cut off to avoid DoS attack
                    while (expected < shutdown.length()) {
                        if (random == null) {
                            random = new Random();
                        }
                        expected += (random.nextInt() % 1024);
                    }
                    while (expected > 0) {
                        int ch = -1;
                        try {
                            ch = stream.read();
                        } catch (IOException e) {
                            log.warn(sm.getString("standardServer.accept.readError"), e);
                            ch = -1;
                        }
                        // Control character or EOF (-1) terminates loop
                        if (ch < 32 || ch == 127) {
                            break;
                        }
                        command.append((char) ch);
                        expected--;
                    }
                } finally {
                    // Close the socket now that we are done with it
                    try {
                        if (socket != null) {
                            socket.close();
                        }
                    } catch (IOException e) {
                        // Ignore
                    }
                }

                // Match against our command string
                boolean match = command.toString().equals(shutdown);
                if (match) {
                    log.info(sm.getString("standardServer.shutdownViaPort"));
                    break;
                } else {
                    log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
                }
            }
        } finally {
            ServerSocket serverSocket = awaitSocket;
            awaitThread = null;
            awaitSocket = null;

            // Close the server socket and return
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }
}    

这里存在一个volatile的boolean变量来控制一个while循环,而这个变量只要不被设置为false则永远循环下去,它的作用是不断通过ServerSocket来获取TCP数据报中的“shutdown”指令,也就是你主动关闭tomcat的执行逻辑。

Thread的字段和构造函数

 /* 线程名 */
 private volatile String name;
 /* 优先级 */
 private int            priority;
 
 private Thread         threadQ;
 private long           eetop;

 /* 是否single_step执行此线程 */
 private boolean     single_step;

 /* 该线程是否是守护线程 */
 private boolean     daemon = false;

 /* JVM 状态 */
 private boolean     stillborn = false;

 /* 将运行的任务. */
 private Runnable target;

 /* 线程分组 */
 private ThreadGroup group;

 /* 此线程的上下文类加载器 */
 private ClassLoader contextClassLoader;

 /* 此线程继承的AccessControlContext */
 private AccessControlContext inheritedAccessControlContext;

 /* 用于自动给匿名线程编号 */
 private static int threadInitNumber;
 private static synchronized int nextThreadNum() {
     return threadInitNumber++;
 }

 /* 与此线程相关的ThreadLocal值。此map由ThreadLocal类维护 */
 ThreadLocal.ThreadLocalMap threadLocals = null;

 /*
  * 与此线程相关的InheritableThreadLocal值。此map由ThreadLocal类维护。
  */
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

 /*
  * 此线程请求的堆栈大小,如果创建者未指定堆栈大小,则为0。
  * 这取决于VM如何利用这个数字;有些虚拟机会忽略它。(JVM会忽略)
  */
 private long stackSize;

 /*
  * 本机线程终止后持续存在的JVM私有状态。
  */
 private long nativeParkEventPointer;

 /*
  * 线程 ID
  */
 private long tid;

 /* 用于生成线程 ID */
 private static long threadSeqNumber;

 /* 
  * tools的Java线程状态,初始化为指示线程“尚未启动”
  */

 private volatile int threadStatus = 0;


 private static synchronized long nextThreadID() {
     return ++threadSeqNumber;
 }

 /**
  * The argument supplied to the current call to
  * java.util.concurrent.locks.LockSupport.park.
  * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
  * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
  */
 volatile Object parkBlocker;

 /* The object in which this thread is blocked in an interruptible I/O
  * operation, if any.  The blocker's interrupt method should be invoked
  * after setting this thread's interrupt status.
  */
 private volatile Interruptible blocker;
 private final Object blockerLock = new Object();

 /* Set the blocker field; invoked via sun.misc.SharedSecrets from java.nio code
  */
 void blockedOn(Interruptible b) {
     synchronized (blockerLock) {
         blocker = b;
     }
 }

 /**
  * 线程可以拥有的最小优先级
  */
 public final static int MIN_PRIORITY = 1;

/**
  * 线程默认分配的优先级。
  */
 public final static int NORM_PRIORITY = 5;

 /**
  * 线程可以拥有的最大优先级
  */
 public final static int MAX_PRIORITY = 10;

public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

run()方法和start()方法的区别?

可以看到,根据构造函数传过来的Runnable对象在这里使用,Thread本身也是实现了Runnable接口,所以就相当于把任务交给Thread了让它来执行。

public void run() {
    if (target != null) {
        target.run();
    }
}

使该线程开始执行;JVM调用该线程的run方法。

结果是两个线程同时运行:当前线程(从调用start方法返回)和另一个线程(执行其run方法)。

多次启动线程是不合法的。特别是,线程一旦完成执行,就不能重新启动。——所以这里使用了synchronized对方法进行修饰。

public synchronized void start() {
    /**
     * 对于VM创建/设置的主方法线程或“系统”组线程,不会调用此方法。
     * 将来添加到此方法的任何新功能可能也必须添加到VM中。——意思就是这里只关注执行任务
     *
     *  0表示线程处于"NEW"状态。
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
        
    /* 通知组此线程即将启动,以便将其添加到组的线程列表中,并减少组的未启动计数。*/ 
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* 不做什么。如果start0抛出了一个可丢弃的,那么它将被传递到调用堆栈中 */
        }
    }
}

本地方法会开启一个线程来调用Thread的run方法。
private native void start0();

所以很明显,run方法就是一个普通的方法,和你平时调用对象的方法一样,是由Caller线程执行。start方法会调用本地方法新建一个线程来执行任务,所以会说是两个线程同时运行。

线程的生命周期是怎样的?

我们可以看到线程的状态值是threadStatus,可以发现它的相关赋值代码是找不到,应该是JVM来控制的。不过我们能找到get方法:

public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

public static State toThreadState(int var0) {
    if ((var0 & 4) != 0) {
        return State.RUNNABLE;
    } else if ((var0 & 1024) != 0) {
        return State.BLOCKED;
    } else if ((var0 & 16) != 0) {
        return State.WAITING;
    } else if ((var0 & 32) != 0) {
        return State.TIMED_WAITING;
    } else if ((var0 & 2) != 0) {
        return State.TERMINATED;
    } else {
        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
    }
}

既然有枚举我们就可以看看官方对各个状态的定义。

NEW

尚未启动的线程处于此状态。

RUNNABLE

在JVM中执行的线程处于这种状态,但它可能正在等待来自操作系统的其他资源,例如处理器。

BLOCKED

被阻止等待监视器锁定的线程处于此状态。比如等待一个synchronized锁。

WAITING

无限期等待另一个线程执行特定操作的线程处于此状态。

以下会触发该状态:

  • Object#wait():等待另一个线程调用此对象的notify或者notifyAll方法。
  • Thread#join()
  • LockSupport#park()

TIMED_WAITING

等待另一个线程执行操作的线程在指定的等待时间内处于此状态。

以下会触发该状态:

  • Thread.sleep
  • Object.wait(J)
  • Thread.join(J)
  • LockSupport#parkNanos
  • LockSupport#parkUntil

TERMINATED

已退出的线程处于此状态。

读者可以根据定义自行实验,下面的图能大致描述不同状态的转变。 image.png

sleep、join、wait的区别?

sleep和wait的区别

  • sleep是位于Thread类中的,wait是位于Object类中的。
  • sleep是让线程进行定时休眠会导致当前线程处于TIMED_WAITTING状态;wait是等待另一个线程调用对象的notify(唤醒正在该对象监视器上等待的单个线程。)或者notifyAll(唤醒正在该对象监视器上等待的所有线程。)方法,指定超时就会导致线程处于TIMED_WAITTING状态,不指定就会导致线程处于WAITTING状态。
  • wait需要给对象加上synchronized锁,如果不加会抛出InterruptedException异常。需要如下:
           synchronized (obj) {
               while (<condition does not hold>)
                   obj.wait();
               ... // Perform action appropriate to condition
           }
           synchronized (obj) {
               ... // create condition
               obj.notify();
           }

为啥wait需要和notify使用同一个synchronized?

这里设进行notify/notifyAll的线程为A线程,wait的线程为B线程。

因为在该模式下需要先等B线程先生产,A线程再消费,如果B和A同时发生的话,很可能B还没生产好A就去消费了个寂寞,后面B生产好了也没线程消费了,所以需要上锁同步。至于为啥synchronized要使用obj而不是其他的对象,因为调用obj.wait()之后就可以释放锁,通知wait停止阻塞后应该会重新加上。

如下:

    @SneakyThrows
    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (lock) {
                    long current = System.currentTimeMillis();
                    System.out.println("准备进行等待");
                    lock.wait();
                    System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
                }
            }
        });
        thread1.start();


        Thread.sleep(2000);
        synchronized (lock) {
            System.out.println("准备进行通知");
            lock.notify();
            System.out.println("通知完成");
        }
    }

输出:

准备进行等待
准备进行通知
通知完成
等待完成,等待了2006

证实下B线程被通知后是不是会重新加上锁:

    @SneakyThrows
    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (lock) {
                    long current = System.currentTimeMillis();
                    System.out.println("准备进行等待");
                    lock.wait();
                    System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
                    Thread.sleep(2000);
                }
            }
        });
        thread1.start();

        Thread thread2 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                long current = System.currentTimeMillis();
                Thread.sleep(2500);
                synchronized (lock) {
                    System.out.println("tthread2获得了锁,执行消耗了"  + (System.currentTimeMillis() - current));
                }
            }
        });
        thread2.start();

        Thread.sleep(2000);
        synchronized (lock) {
            System.out.println("准备进行通知");
            lock.notify();
            System.out.println("通知完成");
        }
    }

输出:

准备进行等待
准备进行通知
通知完成
等待完成,等待了2013
thread2获得了锁,执行消耗了4012

可以看到需要等thread1彻底完成后再释放锁,thread2才能获得锁。

再证实下notify是不是不会管线程有没有消费通知:

    @SneakyThrows
    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                Thread.sleep(1000);
                synchronized (lock) {
                    long current = System.currentTimeMillis();
                    System.out.println("准备进行等待");
                    lock.wait();
                    System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
                }
            }
        });
        thread1.start();

        synchronized (lock) {
            System.out.println("准备进行通知");
            lock.notify();
            System.out.println("通知完成");
        }
    }

输出:

准备进行通知
通知完成
准备进行等待

一直等待

总结

可以发现wait和notify/notifyAll是一个粗糙的生产消费模式,notify不会管等待的线程有没有生产完,只管消费,就算没有就过去了,后面来了也不消费。所以一般不使用它。

底层的逻辑应该是wait方法会释放obj的对象头中的锁标记,把等待的线程的信息保存下来,notify/notifyAll会去保存信息的位置找有没有因为obj#wait导致阻塞的线程,有的话就直接唤醒(如果有阻塞了多个那就一个一个唤醒,因为需要持有锁)并把锁归还。

sleep和join的区别

根据系统计时器和调度程序的精度和准确性,使当前执行的线程休眠(暂时停止执行)指定的毫秒数。线程不会失去任何监视器的所有权(不会像wait一样释放锁)。

public static native void sleep(long millis) throws InterruptedException;

等待此线程死亡的时间最多为毫秒。超时0表示永远等待。使用一个循环条件为this.isAlive内部调用this.wait。当一个线程终止时this.notifyAll方法是被调用。建议应用程序在线程实例上不使用wait、notify或者notifyAll。

public final synchronized void join(long millis)
throws InterruptedException {
    //当前时间戳
    long base = System.currentTimeMillis();
    long now = 0;
    
    //等待时间不能小于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);
            // 防止wait时间不足
            now = System.currentTimeMillis() - base;
        }
    }
}

可以看到sleep是用来实当前线程进行一段时间的休眠,只会导致线程状态处于TIMED_WAITING。而join是用来等待当前线程死亡内部使用wait,线程终止的时候会调用notify/notifyAll,肯定不是用来等待自己的线程死亡,一般用在另一个线程等其他线程死亡。

interrupt、stop的区别

stop

强制停止线程执行。

thread所代表的线程将被迫停止无论它在做什么反常的操作,并将新创建的ThreadDeath对象作为异常抛出。

可以看到stop不建议使用了,因为它会强制线程停止,如果持有锁锁也不会释放。

@Deprecated
public final void stop() {
    SecurityManager security = System.getSecurityManager();
    //如果安装了安全管理器,则调用其checkAccess方法,并将其作为参数。这可能会导致(在当前线程中)引发SecurityException。
    if (security != null) {
        checkAccess();
        //如果此线程与当前线程不同(即,当前线程正在尝试停止自身以外的线程),则会另外调用安全管理器的checkPermission方法。
        if (this != Thread.currentThread()) {
            security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
    // 0表示线程的状态为NEW,它不能为非NEW,因为持有锁。
    if (threadStatus != 0) {
        resume(); // 如果线程被挂起,则唤醒线程;没有别的办法
    }

    // VM可以停止所有状态的线程
    stop0(new ThreadDeath());
}

interrupt

中断线程(只是设置中断标识)。 除非当前线程正在中断自身(这是始终允许的),否则会调用此线程的checkAccess方法,这可能会导致抛出SecurityException。

如果该线程在调用对象类的wait()、wait(long)或wait(long,int)方法,或该类的join()、join(long)、join(long,int)、sleep(long)或sleep(long,int)方法时被阻塞,那么它的中断状态将被清除,并接收到InterruptedException。

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // 只是设置中断标识
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

只能中断处于RUNNABLE状态的线程,可以验证下:

Thread thread2 = new Thread(new Runnable() {
    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            System.out.println("111");
        }
    }
});
thread2.start();

thread2.interrupt();
System.out.println("thread2 isInterrupted:" + thread2.isInterrupted());

输出:

thread2 isInterrupted:true

中断TIMED_WAIT状态的:

Thread thread2 = new Thread(new Runnable() {
    @SneakyThrows
    @Override
    public void run() {
       Thread.sleep(2000);
       System.out.println("thread2完成");
    }
});
thread2.start();

thread2.interrupt();
System.out.println("thread2 isInterrupted:" + thread2.isInterrupted());
thread2 isInterrupted:false
Exception in thread "Thread-1" java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.study.jvm.Demo$2.run(Demo.java:73)
	at java.lang.Thread.run(Thread.java:748)

总结

stop是强制的不安全,interrput只是发送信号,中断处于RUNNABLE状态的线程其实并没有终止,中断处于阻塞或者等待状态的线程会终止并抛出异常。

异步线程想要返回结果咋办?-FutureTask

直接上代码

FutureTask task = new FutureTask<Boolean>(new Callable<Boolean>() {
    @Override
    public Boolean call() throws Exception {
        Thread.sleep(1000);
        return true;
    }
});
Thread thread = new Thread(task);
thread.start();

long current = System.currentTimeMillis();
task.get();
System.out.println("获取结果消耗:" + (System.currentTimeMillis() - current));

get会一直阻塞等待结果。

get的加锁和释放

这里简单说下

image.png

image.png

这里调用了LockSupport.park和ReentrantLock的AQS阻塞一样。那么解除阻塞应该是run方法内部,见下:

image.png

image.png

image.png

线程死锁啥时候发生的?

image.png 直接上代码模拟:


Object lock1 = new Object();
Object lock2 = new Object();

Thread thread1 = new Thread(new Runnable() {
    @SneakyThrows
    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println("thread1成功持有lock1");
            Thread.sleep(1000);
            synchronized (lock2) {
                System.out.println("thread1成功持有lock2");
            }
        }


    }
});

Thread thread2 = new Thread(new Runnable() {
    @SneakyThrows
    @Override
    public void run() {
        synchronized (lock2) {
            System.out.println("thread2成功持有lock2");
            Thread.sleep(1000);
            synchronized (lock1) {
                System.out.println("thread2成功持有lock1");
            }
        }
    }
});

thread1.start();
thread2.start();

输出:

thread1成功持有lock1
thread2成功持有lock2

一直在等待。

线程假死啥时候发生,怎么排查?

假死—线程存在但是没有一直没有任何表现,比如:

  • 没有日志输出
  • 不进行任何作业 大概率是阻塞等待,或者出现了死锁。

可以使用VisualVM、jconsole等等进行排查。查看《Java服务相关问题排查》

参考

wait为什么要在同步块中使用? 为什么sleep就不用再同步块中?