敞开心扉,一起聊聊Java多线程

目录

  • 敞开心扉,一起聊聊Java多线程(结尾有福利~)
    • 一、线程的实现方式
        • 1.继承Thread类,重写run方法
        • 2.实现Runnable接口,重写run方法
        • 3.实现Callable接口重写run方法,通过FutureTask包装器获取返回值
    • 二、线程的生命周期
    • 三、线程状态转换
    • 四、线程的启动
        • 经典面试题
        • 源码剖析
    • 五、线程的终止
        • 1.暴力终止法:stop()
        • 2.自定义标志位终止
        • 3.优雅中断法:interrupt()
    • 六、线程的复位
        • 1.Thread.interrupted()
        • 2.通过抛出InterruptedException异常
  • 福利:梁博-《出现又离开》


敞开心扉,一起聊聊Java多线程(结尾有福利~)

今天!我们来聊一聊 多线程 ~

我们都知道,不论在是面试还是工作中,多线程都是一些老生常谈的话题,

相信正在阅读得你,脑海中已然浮现出多线程的相关知识,那么,我们来一起回顾下吧 ~

注意:本片博文前面内容重点在于回顾,后面内容重点讲解线程的生命周期以及线程的源码剖析


一、线程的实现方式

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法
  • 实现Callable接口重写run方法,通过FutureTask包装器获取返回值

1.继承Thread类,重写run方法

/**
 * 多线程
 *      继承 Thread方式
 * @author zhaojun
 */
public class MyThread extends Thread {

    public MyThread(String name) {
        // 支持自定义线程名称
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
        new MyThread("thread_1").start();
        new MyThread("thread_2").start();
        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
    }

}

测试结果如下:

敞开心扉,一起聊聊Java多线程_第1张图片

2.实现Runnable接口,重写run方法

/**
 * 多线程
 *      实现 Runnable方式
 * @author zhaojun
 */
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
        new Thread(new MyRunnable(), "runnable_1").start();
        new Thread(new MyRunnable(), "runnable_2").start();
        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
    }

}

测试结果如下:

敞开心扉,一起聊聊Java多线程_第2张图片

3.实现Callable接口重写run方法,通过FutureTask包装器获取返回值

/**
 * 多线程
 *      实现 Callable方式, 利用 FutureTask获取返回值
 * @author zhaojun
 */
public class MyCallable implements Callable<String> {

    // Callable接口支持指定泛型,对应call返回值类型为指定泛型
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
        return Thread.currentThread().getName() + "线程当前运行状态为:" + Thread.currentThread().getState();
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {
        FutureTask<String> task1 = new FutureTask<>(new MyCallable());
        new Thread(task1, "callable_1").start();
        System.out.println(task1.get());

        FutureTask<String> task2 = new FutureTask<>(new MyCallable());
        new Thread(task2, "callable_2").start();
        System.out.println(task2.get());

        System.out.println(Thread.currentThread().getName() + " -> " + Thread.currentThread().getId());
    }

}

测试结果如下:

敞开心扉,一起聊聊Java多线程_第3张图片

二、线程的生命周期

  • 线程初始状态:NEW
  • 线程运行状态:RUNNABLE
  • 线程阻塞状态:BLOCKED
  • 线程等待状态:WAITING
  • 超时等待状态:TIMED_WAITING
  • 线程终止状态:TERMINATED

这并不是笔者胡乱编造的,而是jdk源码中定义的(Thread类中维护的一个枚举类),源码如下并加以翻译:

/**
 * 多线程
 *      源码定义 - 翻译
 * @author zhaojun
 */
public enum State {

    /**
     * 线程初始状态
     *      线程被构建,还未调用 start方法
     */
    NEW,

    /**
     * 线程运行状态
     *      JAVA线程把操作系统中的(就绪和运行)两种状态统一称为 运行中
     */
    RUNNABLE,

    /**
     * 线程阻塞状态
     *      表示线程进入等待状态,即线程因为某种原因放弃了 CPU使用权,阻塞也分为几种情况:
     *       1.等待阻塞:运行的线程执行 wait方法,JVM会把当前线程放入到等待队列
     *       2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 JVM会把当前的线程放入到锁池中
     *       3.其他阻塞:运行的线程执行 Thread.sleep或者 join方法,或者发出了 I/O请求时,
     *                  JVM会把当前线程设置为阻塞状态,当 sleep结束/join线程终止、I/O处理完毕则线程恢复
     */
    BLOCKED,

    /**
     * 线程等待状态
     */
    WAITING,

    /**
     * 线程超时等待状态
     *      超时之后自动返回
     */
    TIMED_WAITING,

    /**
     * 线程终止状态
     *      表示当前线程执行完毕
     */
    TERMINATED;
}

三、线程状态转换

此处重点讲下,线程状态如何变更为:
       1.TIME_WAITING:线程超时等待状态
       2.WAITING:线程等待状态
       3.BLOCKED:线程阻塞状态

/**
 * 多线程
 *      状态转换 - 代码演示
 * @author zhaojun
 */
public class ThreadStatus {

    public static void main(String[] args) {

        /**
         * Thread -> TIME_WAITING:线程超时等待状态
         */
        Thread timeWaiting = new Thread(() -> {
            while (true) {
                try {
                    // sleep 99s
                    TimeUnit.SECONDS.sleep(99);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Time_Waiting_Thread");


        /**
         * Thread -> WAITING:线程等待状态
         */
        Thread waiting = new Thread(() -> {
            while (true) {
                synchronized (ThreadStatus.class) {
                    try {
                        // wait
                        ThreadStatus.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Waiting_Thread");


        /**
         * Thread -> BLOCKED:线程阻塞状态
         */
        Thread blocked_thread_01 = new Thread(new BlockedThread(), "Blocked_Thread_01");
        Thread blocked_thread_02 = new Thread(new BlockedThread(), "Blocked_Thread_02");


        /**
         * 启动
         */
        timeWaiting.start();
        waiting.start();
        blocked_thread_01.start();
        blocked_thread_02.start();

    }


    /**
     * 定义阻塞线程类
     */
    static class BlockedThread extends Thread {
        @Override
        public void run() {
            synchronized (BlockedThread.class) {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(99);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}

测试结果如下:

首先运行main方法,在当前测试类任意位置右击,点击Open in Terminal

敞开心扉,一起聊聊Java多线程_第4张图片

输入jps -l命令,查看所有java进程对应pid,查找ThreadStatus对应 pid

敞开心扉,一起聊聊Java多线程_第5张图片

输入 jstack pid命令,查看其堆栈信息:

敞开心扉,一起聊聊Java多线程_第6张图片
从上图中,可以很清晰的看出:
线程Waiting_Thread 通过 wait();,状态 -> 等待状态;
线程Time_Waiting_Thread 通过sleep(),状态 -> 超时等待状态;
线程Blocked_Thread_01先获得锁,然后通过sleep(),状态 -> 超时等待状态;
线程Blocked_Thread_02未获得锁,状态 -> 阻塞状态;

四、线程的启动


经典面试题

为什么启动一个线程调用 start方法,而不是run方法呢?


源码剖析

接下来,带大家来看一下 start()方法,在源码中如何定义的:

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
                       
            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(); // -.- 目光集聚这里


public class Thread implements Runnable {

    /* Make sure registerNatives is the first thing  does. */
    private static native void registerNatives();

    static {
        registerNatives();
    }

}

我们发现 start()实际上调用了 start0()来启动线程,而且 start0()是由native修饰的本地方法
这里先记住,start0()这个方法是在 Thread 的静态块中来注册的。

到这里,我需要给各位开发小伙伴科普一个文件 Thread.c
该文件定义了各个操作系统平台要用的关于线程的公共数据以及操作,代码如下:

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"

#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},  // -.- 目光集聚这里
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

#undef THD
#undef OBJ
#undef STE

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls) // 发现 registerNatives()方法定义在这里
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}


从代码中看到,start0()会执行JVM_StartThread这个方法,那么问题来了,JVM_StartThread又是什么呢?俗话说,码如其名,先从名字推断应该是在 JVM层启动一个线程,既然有了猜想,我们不妨去验证下。

注意:这里需要下载 hotspot的源码,它是JVM的具体实现,有兴趣的小伙伴可以自行下载

不过,大家莫慌,针对线程启动的源码我会附上
我们找到 jvm.cpp文件,源码如下:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

...

native_thread = new JavaThread(&thread_entry, sz);

...

从代码中可以看出,JVM_ENTRY用来定义JVM_StartThread函数的,在这个函数中创建了一个真正的和平台有关的本地线程,之后new了一个javaThread,看看其具体做了什么:

再找到 thread.cpp文件,源码如下:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;

}

此函数有两个参数:

1.函数名称,线程创建成功之后根据函数名称调用对应函数;

2.当前进程内已有的线程数量

在上述代码19行,os::create_thread,是调用平台创建线程的方法从而来进行创建线程,接下来就是线程的启动了,代码如下:

void Thread::start(Thread* thread) {
  trace("start", thread);
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
      // Initialize the thread state to RUNNABLE before starting this thread.
      // Can not set it after the thread started because we do not know the
      // exact thread state at that time. It could be in MONITOR_WAIT or
      // in SLEEPING or some other state.
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

上述代码14行,os::start_thread(thread)就是平台启动线程的方法,最终会调用 Thread.cpp文件中的 JavaThread::run()方法,至此,一个线程的启动就完成了。

五、线程的终止

  • 暴力终止法:stop()
  • 自定义标志位终止
  • 优雅中断法:interrupt()

1.暴力终止法:stop()

敞开心扉,一起聊聊Java多线程_第7张图片
如图所示,官方已将 stop()定义为过时方法,并不建议使用;
stop()方法在结束一个线程时并不会保证线程的资源正常释 放,因此会导致程序可能出现一些不确定的状态。

JDK官方文档中定义弃用这些方法的原因,链接如下:
Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?

2.自定义标志位终止

首先我们知道,一个线程结束与否取决于当前线程的 run()方法是否执行完毕,我们可自定义标志位来控制线程的结束,代码如下:

/**
 * 多线程
 *      线程终止:自定义标志位
 */
public class ThreadFlagDemo extends Thread {

    // 标志位
    private volatile boolean isFinish = false;

    private static int i;

    @Override
    public void run() {

        while (!isFinish) {
            i++;
        }

        System.out.println("i:"+i);
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {

        ThreadFlagDemo thread = new ThreadFlagDemo();
        thread.start();
        Thread.sleep(1000);

        // 变更标志位,结束线程
        thread.isFinish = true;

    }

}

如代码所示,我们可以通过自定义一个标志位,来结束死循环致使run()结束,从而来控制线程的结束。

3.优雅中断法:interrupt()

这里需明确一点,调用 interrupt() 方法,表示向当前线程打个招呼,告诉其可以中断线程了,至于什么时候终止,取决于当前线程自己,其实原理跟自定义标志位相似,只是打一个停止的标志,并不会去真的停止线程。

/**
 * 多线程
 *      线程终止:interrupt()方法
 */
public class ThreadInterruptDemo {

    private static int i;

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {

            // isInterrupted()默认为false
            while (!Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("i:"+i);

        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);

        // 将isInterrupted()设置为true
        thread.interrupt();

    }

}

这种通过标志位或中断操作的方式能够使线程在终止时可以继续执行内部逻辑,而不是立即停止线程,所以,这种中断线程的方式更加的优雅安全,推荐此种方式

六、线程的复位

  • Thread.interrupted()
  • 通过抛出InterruptedException异常

1.Thread.interrupted()

/**
 * 多线程
 *      线程复位:Thread.interrupted()方法
 */
public class ThreadInterruptedReset {

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            while (true) {
                if(Thread.currentThread().isInterrupted()) {

                    System.out.println(threadName + ":before -> " + Thread.currentThread().isInterrupted());

                    // 线程复位
                    Thread.interrupted();

                    System.out.println(threadName + ":after -> " + Thread.currentThread().isInterrupted());

                    break; // 结束
                }
            }
        }, "Thread_Interrupted");
        thread.start();
        TimeUnit.SECONDS.sleep(1);

        // 将isInterrupted()设置为true
        thread.interrupt();

    }

}

执行流程如下:

  1. 执行main()方法,标识"main"主线程启动,代码自上而下执行
  2. "Thread_Interrupted"线程启动,while循环开启,isInterrupted()默认false,…当前处在死循环中;
  3. 与此同时,"main"主线程sleep 1s结束后,将 isInterrupted()设置为true;
  4. 此时,"Thread_Interrupted"线程中,由于 isInterrupted()当前被设置为true,执行 if块代码,before -> true;
  5. 通过 Thread.interrupted()进行复位,after -> false,最终执行 break,结束循环结束线程。

执行流程如下:

敞开心扉,一起聊聊Java多线程_第8张图片

2.通过抛出InterruptedException异常

/**
 * 多线程
 *      线程复位:InterruptedException异常
 */
public class ThreadExeptionReset {

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            while (true) {
                if(Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println(threadName + ":before -> " + Thread.currentThread().isInterrupted());
                        TimeUnit.SECONDS.sleep(10);

                    } catch (InterruptedException e) {
                        System.out.println(threadName + ":after -> " + Thread.currentThread().isInterrupted());
                        e.printStackTrace();
                        break; // 结束
                    }
                }
            }
        }, "Thread_InterruptedException");
        thread.start();
        TimeUnit.SECONDS.sleep(1);

        // 将isInterrupted()设置为true
        thread.interrupt();

    }

}

执行流程如下:

  1. 执行main()方法,标识"main"主线程启动,代码自上而下执行
  2. "Thread_InterruptedException"线程启动,while循环开启,isInterrupted()默认false,…当前处在死循环中;
  3. 与此同时,"main"主线程sleep 1s结束后,将 isInterrupted()设置为true;
  4. 此时,"Thread_InterruptedException"线程中,由于 isInterrupted()当前被设置为true,执行 if块代码,before -> true;
  5. 接着执行sleep(),抛出InterruptedException异常进行复位,after -> false,最终执行 break,结束循环结束线程。

执行流程如下:

敞开心扉,一起聊聊Java多线程_第9张图片


福利:梁博-《出现又离开》


今日推荐单曲,梁博先生的《出现又离开》,放松心情,努力学习~
 
差点忘记件比写博客还要重要的事情 ~
这里有 博哥(梁博)的粉丝么, 听说今年有博哥的演唱会, 私信我,约起哈 ~ ❤

你可能感兴趣的:(并发编程系列,并发编程,java)