百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

敞开心扉,一起聊聊Java多线程(java多线程讲解)

nanshan 2024-11-10 10:12 13 浏览 0 评论

敞开心扉,一起聊聊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());
    }

}

测试结果如下:

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

}

测试结果如下:

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

}

测试结果如下:

二、线程的生命周期

  • 线程初始状态: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:

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

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

从上图中,可以很清晰的看出:线程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 <clinit> 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()

如图所示,官方已将 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,结束循环结束线程。

执行流程如下:

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,结束循环结束线程。

执行流程如下:

相关推荐

三种自建KMS激活系统自动激活windows方法

第一种:在windows服务器上搭建主要针对vol版本(win7、win10、win20xx、win2012等等)平台:我自己搭建的windows虚拟机,windows2016的操作系统软件:...

重装系统被收98元?避开Windows付费陷阱的实用指南

重装系统被收98元?避开Windows付费陷阱的实用指南有网友反映,在重装Windows系统后,屏幕突然弹出“激活系统需支付98元服务费”的提示,疑惑自己是不是遭遇了付费陷阱。事实上,微软官方的Wi...

Windows Server2012远程桌面服务配置和授权激活

安装:注意:安装完毕之后需手动重启一下计算机配置终端服务管理工具---远程桌面服务---RD授权诊断程序,查看当前服务器有没有授权授权:运行—>gpedit.msc->计算机配置---管理...

新书速览|Windows Server 2022 系统与网站配置实战

讲述桌面体验、ServerCore/NanoServer,容器与云系统的配置1本书内容《WindowsServer2022系统与网站配置实战》秉持作者一贯理论兼具实践的写作风格,以新版的Wi...

Windows激活全攻略:KMS神钥与专业工具的完美结合!

对于许多Windows用户来说,系统的激活是一个必经的过程。虽然Windows操作系统在未经激活的状态下也可以使用一段时间,但长期来看,未激活的系统会限制某些功能并频繁提示用户激活。以下是两种流行的激...

微软Win9全新激活技术曝光(微软系统激活有什么用)

2014-07-0905:46:00作者:徐日俄罗斯Wzor日前披露了更多关于Windows9的最新消息,据悉,Windows9将会在今年秋季亮相,其宣传口号是“想要开始按钮和开始菜单?如你所...

快速激活Windows 10/11:CMD命令详细教程

#记录我的2024#激活Windows操作系统是确保系统功能和安全更新正常运行的重要步骤。本文将为您分享如何使用命令提示符(CMD)在Windows10和Windows11上进行激活的详细步骤。...

Wndows 2019 RDS应用发布部署(rds的安装和应用程序的发布)

安装前的准备1、需要提供服务器作为应用中心,应用中心的推荐配置如下表所示。规格建议1-10人11-20人21-50人51-100人100+人CPU4核8核16核内存8GB16GB32GB64GB系统盘...

解决 Windows 系统激活难题(如何解决windows激活问题)

今天,一位朋友给我说,他手头有三台电脑,均同时弹出系统未激活的提示。他对此毫无头绪,便急忙将电脑上出现的激活提示信息一股脑发给了我。我看到其中一台显示的是“Windows10企业版LTSC尚...

自建KMS激活服务器(自建kms激活服务器的风险)

自建KMS激活服务器Win10和office安装后,都需要激活才可以使用,一般可以输入购买的MAK激活码进行在线激活,也可以通过KMS激活,网上也有很多激活工具,但这些工具一般都含有病毒或木马程序,容...

30秒免费激活windows和office亲测有效!

“第三方工具有病毒?”“KMS服务器激活总失效?”今天给大家分享一个开源激活工具——MicrosoftActivationScripts(MAS),无需密钥、不装软件,30秒永久激活Window...

「操作系统」Windows 10 LTSC 2019 企业版C大集成更新版

Windows10LTSC企业版CHIANNET集成更新优化整合多镜像版,CHIANNET,是USBOS超级PE维护盘工具箱作者,长久以来一直默默的更新着,USBOSPE软件,电脑城装机及...

一文看懂Windows激活:自查方法+授权类型科普(Win7/Win10通用)

一、如何判断Windows是否永久激活?无论是Win7还是Win10,均可通过以下方法快速验证:命令提示符法(通用):按下Win+R,输入slmgr.vbs/xpr并按回车键运行即可查看是否...

部分Windows Server 2019/2022用户反馈无法运行微软Teams应用

IT之家7月2日消息,科技媒体borncity今天(7月2日)发布博文,报道称在多个WindowsServer版本上,MicrosoftTeams应用近期出现了运行故障。用...

这种Windows激活方式已有20年...(windows现在激活)

2006年微软正式发布WindowsVista,随之而来引入了一项新的激活机制「OEM激活」,这项机制在Vista和Win7上最为流行。其实WindowsServer自2008开始至2025版本一...

取消回复欢迎 发表评论: