Java并发编程基础篇(一)之线程

Java并发编程基础篇(一)之线程
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码

前言

并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最 大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会 面临非常多的挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题。这一块内容也是面试核心考点之一,所以博主将以线程为起点,从0到1一起与小伙伴们走去Java并发编程之路上走一遭!

正文

进程?线程?傻傻分不清?

何谓进程

进程通常是程序、应用的同义词。进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。下面来一张图,大致了解一下并发进程张什么样子:
在这里插入图片描述
有了进程以后,可以让操作系统从宏观层面实现多应用并发。而并发的实现是通过 CPU 时间片不端切换执行的。对于单核 CPU 来说,在任意一个时刻只会有一个进程在被CPU 调度。

既生进程,何生线程

  1. 在多核 CPU 中,利用多线程可以实现真正意义上的并行执行。
  2. 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性。
  3. 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快。

何谓线程

线程(Thread)的英文原意是“细丝”,Java语音上把“正在执行程序的主体”称为线程(Thread)。在一个软件内,我们也能同时干多个事,这些不同的功能可以同时进行,是因为在进程内使用了多个线程。线程有时又称之为轻量级进程。但是创建一个线程要消耗的资源通常比创建进程少的多。
注:不是只有Java程序处理系统上执行的才叫线程,垃圾收集器回收垃圾执行的也叫线程,还有GUI相关线程等等。

线程与进程的关系

一个进程内的多个线程会共享进程的资源,同时也会有自己私有的资源。线程必须存在于进程中,每个进程至少要有一个线程作为程序的入口。线程是可以并发执行的,所以我们在一个软件内也可以同时干多个事。操作系统上通常会同时运行多个进程,每个进程又会开启多个线程。一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

Java中线程的分类

Java线程主要分为两类:用户线程(非守护线程)和守护线程。用户线程是应用程序运行时执行在前台的线程,守护线程是应用程序运行执行在后台的线程,也就是程序运行的时候在后台为非守护线程提供一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程的没啥本质的区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

单线程程序

想一想我们读代码的一个过程,接下来这句话,你细品。明为追踪处理流程,实则实在追踪线程。接下来我以图解形式描述一下:
在这里插入图片描述
解读代码的过程就是一个追踪流程的过程,也是一个追踪线程的过程。我们将代码比作一条没有分支大河,无论是方法调用、for循环、if判断还是更为复杂的处理都无所谓,只要这个程序的处理流程从头到尾就只有一条线,那么这个程序就是单线程程序。在单线程程序中,“正在执行程序的主体”永远只有一个。

线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。下面以流程图方式展现一个线程完整的生命周期:
在这里插入图片描述

  1. 新建状态
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  2. 就绪状态
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  3. 运行状态
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  4. 阻塞状态
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  5. 死亡状态
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

public final void setPriority(int newPriority)  :  更改此线程的优先级。 
static int MAX_PRIORITY :线程可以拥有的最大优先级。  (值 为10)
static int MIN_PRIORITY :线程可以拥有的最小优先级。  (值 为1)
static int NORM_PRIORITY :被分配给线程的默认优先级。  (值为5)

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级并不能保证线程执行的顺序,也就是说优先级高的线程不一定就会先执行,因为它有很大的随机性,只有抢到CPU资源的线程才会执行,优先级高的线程只是抢占到CPU资源的机会要更大一点,线程的执行顺序真正取决于CPU调度器(在Java中是JVM来调度),我们是无法控制。
这里也为小伙伴们准备了一段代码,你们可以多运行几次看看效果体会一下优先级:

public class PriorityDemo {
    public static void main(String[] args) {
        //创建线程max最大
        Thread max=new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("max");
                }
            }
        };
        //创建线程min最小
        Thread min = new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("min");
                }
            }
        };
        //创建线程norm默认
        Thread norm = new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println("norm");
                }
            }
        };

        max.setPriority(Thread.MAX_PRIORITY);//将线程max设置为最大值10
        min.setPriority(Thread.MIN_PRIORITY);//将线程min设置为最小大值1

        min.start();
        norm.start();
        max.start();
    }
}

如何创建线程

1.通过实现 Runnable 接口来创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。为了实现 Runnable,一个类只需要执行一个方法调用 run(),线程创建之后调用它的 start() 方法它才会执行。
Thread 定义了几个构造方法,下面的这个是我们经常使用的:

//threadOb 是一个实现 Runnable 接口的类的实例
//threadName是线程的名字
Thread(Runnable threadOb,String threadName);

代码示例:

public class RunnableDemo implements Runnable{
    private Thread thread;
    private String threadName;

    RunnableDemo( String name) {
        threadName = name;
        System.out.println("Creating " +  threadName );
    }

    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 1; i <= 2; i++) {
                System.out.println("Thread: " + threadName + ",第" + i + "次执行");
                // 让线程睡眠一会
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }

    public void start () {
        System.out.println("Starting " +  threadName );
        if (thread == null) {
            thread = new Thread (this, threadName);
            thread.start ();
        }
    }

    public static void main(String[] args) {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();

        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}

执行结果:
在这里插入图片描述
2.通过继承Thread来创建线程

创建一个类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

代码示例:

public class ThreadDemo extends Thread {
    private Thread thread;
    private String threadName;

    ThreadDemo(String name) {
        this.threadName = name;
        System.out.println("Creating " +  threadName );
    }

    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 1; i <= 2; i++) {
                System.out.println("Thread: " + threadName + ",第" + i + "次执行");
                // 让线程睡眠一会
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " finish.");
    }

    public void start () {
        System.out.println("Starting " +  threadName );
        if (thread == null) {
            thread = new Thread (this, threadName);
            thread.start ();
        }
    }

    public static void main(String[] args) {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();

        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}

执行结果:
在这里插入图片描述
给小伙伴们留一个小思考,多执行几次,你会发现执行结果不一样,为什么呢?

3.通过 Callable 和 Future 创建线程

有的时候我们可能需要让一步执行的线程在执行完成以后,提供一个返回值给到当前的主线程,主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程了。创建步骤如下:

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

代码示例:

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args) {
        CallableThreadTest callable = new CallableThreadTest();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        for(int i = 0;i < 3;i++) {
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
            if(i==2) {
                new Thread(futureTask,"有返回值的线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
    @Override
    public Integer call() {
        int i = 0;
        for(;i<3;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}

执行结果:
在这里插入图片描述

Thread常用API

被Thread对象调用的方法:

public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public final void setName(String name)
改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority)
更改线程的优先级。
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
public final void join(long millisec)
等待该线程终止的时间最长为 millis 毫秒。
public void interrupt()
中断线程。
public final boolean isAlive()
测试线程是否处于活动状态。

Thread类的静态方法:

public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

实例演示Thread类的一些方法,做一个“猜数字”小游戏:

C-1:

// 通过实现 Runnable 接口创建线程
public class DisplayMessage implements Runnable {
    private String message;

    public DisplayMessage(String message) {
        this.message = message;
    }

    public void run() {
        while(true) {
            System.out.println(message);
        }
    }
}

C-2:

// 通过继承 Thread 类创建线程
public class GuessANumber extends Thread {
    private int number;

    public GuessANumber(int number) {
        this.number = number;
    }

    public void run() {
        int counter = 0;
        int guess = 0;
        do {
            guess = (int) (Math.random() * 100 + 1);
            System.out.println(this.getName() + "猜的数字是:" + guess);
            counter++;
        } while(guess != number);
        System.out.println(this.getName() + "猜了" + counter + "次猜对正确答案!");
    }
}

C-3:

public class ThreadApiDemo {
    public static void main(String [] args) {
        Runnable hello = new DisplayMessage("Hello");
        Thread thread1 = new Thread(hello);
        thread1.setDaemon(true);
        thread1.setName("hello");
        System.out.println("Starting hello thread...");
        thread1.start();

        Runnable bye = new DisplayMessage("Goodbye");
        Thread thread2 = new Thread(bye);
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread2.setDaemon(true);
        System.out.println("Starting goodbye thread...");
        thread2.start();

        Thread thread3 = new GuessANumber(88);
        System.out.println("Starting thread3...");
        thread3.start();
        try {
            thread3.join(100);
        }catch(InterruptedException e) {
            System.out.println("Thread interrupted.");
        }
        Thread thread4 = new GuessANumber(66);
        System.out.println("Starting thread4...");
        thread4.start();
        System.out.println("main() is ending...");
    }
}

这里执行结果太长,就不演示啦!小伙伴们可以多运行几次看看效果!

线程的优点

恰当的使用线程,可以降低开发和维护的开销,并且能够提高复杂应用程序的性能。线程通过把异步的工作流程转化为普遍存在的顺序流程,使得程序模拟人类工作和交互变得更加容易。另一方面,它可以吧复杂的、难以理解的代码转化为直接、简介的代码,这样更容易读写和维护。

线程在GUI程序中是肥肠有用的,可以用来改进用户接口的响应性,并且在服务器应用中用来提高资源的利用率和吞吐量。它也可以简化JVM的实现------垃圾收集器通常用于一个或多个持续工作的线程之间。大部分至关重要的Java应用都依赖于线程,某种程度上是因为他们的组织结构需要线程。

线程无处不在

即使你的程序中没有明显的创建线程,你所用的框架也可能帮你创建了一些线程,这些程序调用的代码必须是线程安全的。这一点给开发人员的设计和实现赋予了更重要的一份责任,因为开发线程安全的类要比非线程安全的类需要更多的精力和分析。通过从框架中调用应用程序的组件,框架把并发引入了应用程序,组件总是需要访问程序的状态,因此要求在所有代码路径访问状态的时候,必须是线程安全的。

前方高能,心脏不好的小伙伴可以在这里下车了啦!哈哈开个玩笑,前面的内容都是你必备的基础,别看线程简单,面试官要是真拿线程为难你,你可能真的扛不住“夺命连环追问”,接下来我们就一起来对线程最核心的两个点(一个启动,一个终止)进行深入分析。

线程的启动原理

前面我们通过一些案例演示了线程的启动,也就是调用start()方法去启动一个线程,当 run() 方法中的代码执行完毕以后,线程的生命周期也将终止。调用 start ()方法的语义是当前线程告诉 JVM,启动调用 start() 方法的线程。
很多小伙伴最开始可能会跟我一样有一个疑惑,启动一个线程为什么调用的是start()方法而不是run()方法,直接用一个run()方法启动带执行它不香吗?那么带着这个疑惑,下面我们做一个简单的分析,首先看一下start()方法的源码:

  public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        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();//***再看这里***

我们看到调用 start 方法实际上是调用一个 native 方法start0()来启动一个线程,首先 start0()这个方法是在Thread 的静态块中来注册的,代码如下:

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

有些小伙伴可能会有疑问,这个registerNatives()方法是干嘛的?registerNatives()方法是为了让JVM找到你的本地函数,它们必须以某种方式命名。例如,对于java.lang.Object.registerNatives,相应的C函数名为Java_java_lang_Object_registerNatives。通过使用registerNatives(或者更确切地说,JNI函数RegisterNatives),你可以命名你想要的C函数。看一下Thread.c的主要内容:

	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)
    {
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
    }

看methods数组可以看出数组中存放的为JNINativeMethod类型的结构体变量。JNINativeMethod主要是进行一个jni方法的映射关系,将native方法和真正的实现方法进行绑定。那么它是怎么绑定的呢?我来看数组下面的那个方法(Java_java_lang_Thread_registerNatives),它是registerNatives()对应的Jni方法,会对数组methods中的方法映射关系进行注册,注册之后它就会把Java中很多被native修饰的方法与具体实现这个功能的C语音方法的指针进行绑定(这句话可能比较绕,多看几遍)。比如start0()就和JVM_StartThread进行了绑定,那么接下来我们就去看看被JVM_StartThread指针所指向的方法:

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

  {

    MutexLocker mu(Threads_lock);
    //判断Java线程是否已经启动,如果已经启动过,则会抛异常
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      size_t sz = size > 0 ? (size_t) size : 0;
      //如果Java线程没有启动就创建Java线程
      native_thread = new JavaThread(&thread_entry, sz);
      if (native_thread->osthread() != NULL) {
        native_thread->prepare(jthread);
      }
    }
  }

  ......
  Thread::start(native_thread);

JVM_END

这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 thread_entry。Java线程的具体创建过程就在JavaThread的构造函数中,接着看一下JavaThread的构造函数:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  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);
}

我们来看最后一句:os::create_thread(this, thr_type, stack_sz) ,看到os(操作系统)和create_thread就算看不懂C,我想大概也猜的出来这里是在创建Java线程对应的内核线程吧!然后继续看:

bool os::create_thread(Thread* thread, ThreadType thr_type,
                       size_t req_stack_size) {
    ......
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
    ......
    return true;
}

这里pthread_create()的作用就是创建线程,前两个参数先不关注它,我们来看后两个参数分别代表什么,第三个参数thread_native_entry创建的线程开始运行的地址,第四个参数thread代表线程,也是线程函数thread_native_entry()的参数,接着看thread_native_entry函数干了什么:

static void *thread_native_entry(Thread *thread) {
  ......
  thread->run();
  ......
  return 0;
}

是不是看到run()方法了,先别激动,还没结束,这是JDK底层的run()方法,这跟我们Java中的run()有什么关系呢?为什么Java中执行线程也叫作run()方法?run()方法为什么会执行?带着这些疑问,继续看:

// thread.cpp
void JavaThread::run() {
  ......
  thread_main_inner();
}

run()方法中调用了thread.cpp中的thread_main_inner()方法:

void JavaThread::thread_main_inner() {
  if (!this->has_pending_exception() &&
      !java_lang_Thread::is_stillborn(this->threadObj())) {
    {
      ResourceMark rm(this);
      this->set_native_thread_name(this->get_thread_name());
    }
    HandleMark hm(this);
    this->entry_point()(this, this);//重点看这里
  }
  DTRACE_THREAD_PROBE(stop, this);
  this->exit(false);
  delete this;
}

这里结合JavaThread的构造方法一起看,entry_point()方法的返回值就是创建JavaThread时传入的一个函数thread_entry,所以这里就相当于thread_entry(this,this),这个函数在jvm.cpp中:

// jvm.cpp
static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,//Java线程对象
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),//Java线程类
                          vmSymbols::run_method_name(),//开始调用Java线程对象的run()方法
                          vmSymbols::void_method_signature(),
                          THREAD);
}

呦呵!看到谁了?这不是JavaCalls嘛!有小伙伴可能会问JavaCalls是干嘛的,其实它就是用来调用Java方法的。如果对这一块不熟悉,可以点这里JVM方法执行的来龙去脉,可以看到调用了 vmSymbolHandles::run_method_name 方法,而run_method_name是在 vmSymbols.hpp 用宏定义的,它又是怎么对应找到Java中的run()方法的呢?各位看官接下来就解开谜底:

class vmSymbolHandles: AllStatic {
   ...
    template(run_method_name,"run")  
   ...
}

看的很清楚,这个用“双引号”引起来的“run”,就是决定了调用Java代码中的run()方法的关键!

Java线程创建调用图:
在这里插入图片描述

这样分析下来,我相信小伙伴们已经很清楚start()和run()的关系了吧!线程启动使用start(),run()只是一个回调方法而已。如果你直接调用run()方法,JVM是不会去创建线程的,run()方法只能用于已有线程中。Java里面创建线程之后必须要调用start方法才能真正的创建一个线程,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run方法进行业务逻辑的处理。

优雅的线程终止

如何优雅的终止一个线程,也是面试当中经常会被问到的一个问题,接下来我们就研究一下如何优雅的终止一个线程。线程的终止,并不是简单的调用 stop 命令去。虽然 api 仍然可以调用,但是和其他的线程控制方法如 suspend、resume 一样都是过期了的不建议使用,就拿 stop 来说,stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态(相当于我们在linux上通过kill -9强制结束一个进程)。要优雅的去中断一个线程,在线程中提供了一个 interrupt方法。

interrupt 方法

当其他线程通过调用当前线程的 interrupt 方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查自身是否被中断来进行相应,可以通过
isInterrupted()来检查自身是否被中断。
下面我们通过一个实例感受一下线程的终止:

public class InterruptDemo {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                //默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
                i++;
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt(); //加和不加的效果小伙伴们可以感受一下
    }
}

执行结果就是一个简单的打印,打印了线程启动到终止过程中循环执行的次数。通过简单的代码分析我们不难看出,通过 thread.interrupt()方法去改变了中断标识的值使得main方法中while循环的判断不成立而跳出循环,因此main方法执行完毕以后线程就终止了。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是果断地将线程立马停止,因此这种终止线程的做法显得更加安全和优雅。

Thread.interrupted

通过 interrupt方法可以设置一个标识告诉线程可以终止了,线程中还提供了静态方Thread.interrupted()对设置中断标识的线程复位。比如在上面的案例中,外面的线程调用 thread.interrupt 来设置中断标识,而在线程里面,又通过 Thread.interrupted 把线程的标识又进行了复位。实例代码如下:

public class InterruptedDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(true){
                if(Thread.currentThread().isInterrupted()){
                    //默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
                    System.out.println("复位之前:"+Thread.currentThread().isInterrupted());
                    //对线程进行复位,由 true 变成 false
                    Thread.interrupted();
                    System.out.println("复位之后:"+Thread.currentThread().isInterrupted());
                }
            }
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
    }
}

其他的线程复位
除了通过 Thread.interrupted 方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是抛出了一个InterruptedException 异常,在InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false,接下来分别通过下面两个 demo 来进行演示:

Demo - 1:

public class InterruptDemoOne {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("线程执行");
            while(!Thread.currentThread().isInterrupted()){
                //默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
                i++;
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
        System.out.println("未通过异常进行复位,线程是否终止:" + thread.isInterrupted());
    }
}

执行结果:
在这里插入图片描述
Demo - 2:

public class InterruptDemoTwo {
    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("线程执行");
            while(!Thread.currentThread().isInterrupted()){
                //默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
                try {
                    Thread.sleep(1000);//睡眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Num:"+i);
        },"interruptDemo");
        thread.start();
        Thread.sleep(1000);//睡眠一秒
        thread.interrupt();
        System.out.println("通过异常进行复位,线程是否终止:" + thread.isInterrupted());
    }
}

执行结果:
在这里插入图片描述
注:当在sleep中的线程被调用interrupt方法时会放弃暂停的状态,并抛InterruptedException异常

为什么要复位
Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是 false,这就是复位的原因。

线程终止的原理
首先来看一下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();
    }

	...
	
	private native void interrupt0();

我们看到调用 interrupt方法实际上是调用一个 native 方法interrupt0()来终止一个线程,首先 interrupt0()这个方法是在Thread 的静态块中来注册的,跟start0()注册的方式是一样的,前面几步的分析过程与启动方法start()是一样的,这里就不重复了,直接看JVM_Interrupt的定义:

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");
  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

这个方法比较简单,直接调用了 Thread::interrupt(thr)这个方法,这个方法的定义在Thread.cpp文件中,代码如下:

void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

Thread::interrupt方法调用了os::interrupt方法,这个是调用平台的interrupt方法,这个方法的实现是在 os_*.cpp文件中,其中星号代表的是不同平台,因为jvm是跨平台的,所以对于不同的操作平台,线程的调度方式都是不一样的。我们以os_linux.cpp文件为例:

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  //获取本地线程对象
  OSThread* osthread = thread->osthread();
  if (!osthread->interrupted()) {//判断本地线程对象是否为中断
    osthread->set_interrupted(true);//设置中断状态为true
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    //这里是内存屏障,内存屏障的目的是使得interrupted状态对其他线程立即可见
    OrderAccess::fence();
    //_SleepEvent相当于Thread.sleep,表示如果线程调用了sleep方法,则通过unpark唤醒
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }
  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();
  //_ParkEvent用于synchronized同步块和Object.wait(),这里相当于也是通过unpark进行唤醒
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;
}

通过上面的代码分析可以知道,thread.interrupt()方法实际就是设置一个interrupted状态标识为true、并且通过ParkEvent的unpark方法来唤醒线程。这里要注意一下几点:

  1. 对于synchronized阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被park。
  2. 在调用ParkEvent的park方法之前,会先判断线程的中断状态,如果为true,会清除当前线程的中断标识。
  3. Object.wait、Thread.sleep、Thread.join会抛出InterruptedException。

InterruptedException出现的原因

这个异常的意思是表示一个阻塞被其他线程中断了。然后,由于线程调用了interrupt()中断方法,那么Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException。需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如直接捕获异常不做任何处理、将异常往外抛出、停止当前线程,并打印异常信息。为了让大家能够更好的理解上面这段话,我们以Thread.sleep为例直接从jdk的源码中找到中断标识的清除以及异常抛出的方法代码。

找到 is_interrupted()方法,linux平台中的实现在os_linux.cpp文件中,代码如下:

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  OSThread* osthread = thread->osthread();
  bool interrupted = osthread->interrupted(); //获取线程的中断标识
  if (interrupted && clear_interrupted) {//如果中断标识为true
  	//设置中断标识为false,这也是为什么默认情况下isInterrupted 返回 false的原因
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }
  return interrupted;
}

然后在jvm.cpp文件中找到Thread.sleep这个操作在jdk中的源码体现:

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");
  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //判断并清除线程中断状态,如果中断状态为true,抛出中断异常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }
  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  JavaThreadSleepState jtss(thread);

我相信小伙伴们看到源码中的注释一定会豁然开朗,其实启动原理与终止原理很相似,包括stop之类的其他一些Thread类中用native修饰的方法分析流程都是这样。

原文链接:https://gper.club/articles/7e7e7f7ff4g5agc5g6b

本文来源Java实战团,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/7935

发表评论