Java高并发学习笔记:Thread详解

Posted 氷泠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发学习笔记:Thread详解相关的知识,希望对你有一定的参考价值。

1 来源

  • 来源:《Java高并发编程详解 多线程与架构设计》,汪文君著
  • 章节:第一、二、三章

本文是前三章的笔记整理。

2 概述

本文主要讲述了线程的生命周期、Thread类的构造方法以及常用API,最后介绍了线程的关闭方法。

3 线程生命周期

3.1 五个阶段

线程生命周期可以分为五个阶段:

  • NEW
  • RUNNABLE
  • RUNNING
  • BLOCKED
  • TERMINATED

3.2 NEW

new创建一个Thread对象时,但是并没有使用start()启动线程,此时线程处于NEW状态。准确地说,只是Thread对象的状态,这就是一个普通的Java对象。此时可以通过start()方法进入RUNNABLE状态。

3.3 RUNNABLE

进入RUNNABLE状态必须调用start()方法,这样就在JVM中创建了一个线程。但是,线程一经创建,并不能马上被执行,线程执行与否需要听令于CPU调度,也就是说,此时是处于可执行状态,具备执行的资格,但是并没有真正执行起来,而是在等待被调度。

RUNNABLE状态只能意外终止或进入RUNNING状态。

3.4 RUNNING

一旦CPU通过轮询或其他方式从任务可执行队列中选中了线程,此时线程才能被执行,也就是处于RUNNING状态,在该状态中,可能发生的状态转换如下:

  • 进入TERMINATED:比如调用已经不推荐的stop()方法
  • 进入BLOCKED:比如调用了sleep()/wait()方法,或者进行某个阻塞操作(获取锁资源、磁盘IO等)
  • 进入RUNNABLECPU时间片到,或者线程主动调用yield()

3.5 BLOCKED

也就是阻塞状态,进入阻塞状态的原因很多,常见的如下:

  • 磁盘IO
  • 网络操作
  • 为了获取锁而进入阻塞操作

处于BLOCKED状态时,可能发生的状态转换如下:

  • 进入TERMINATED:比如调用不推荐的stop(),或者JVM意外死亡
  • 进入RUNNABLE:比如休眠结束、被notify()/nofityAll()唤醒、获取到某个锁、阻塞过程被interrupt()打断等

3.6 TERMINATED

TERMINATED是线程的最终状态,进入该状态后,意味着线程的生命周期结束,比如在下列情况下会进入该状态:

  • 线程运行正常结束
  • 线程运行出错意外结束
  • JVM意外崩溃,导致所有线程都强制结束

4 Thread构造方法

4.1 构造方法

Thread的构造方法一共有八个,这里根据命名方式分类,使用默认命名的构造方法如下:

  • Thread()
  • Thread(Runnable target)
  • Thread(ThreadGroup group,Runnable target)

命名线程的构造方法如下:

  • Thread(String name)
  • Thread(Runnable target,Strintg name)
  • Thread(ThreadGroup group,String name)
  • Thread(ThreadGroup group,Runnable target,String name)
  • Thread(ThreadGroup group,Runnable target,String name,long stackSize)

但实际上所有的构造方法最终都是调用如下私有构造方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);

在默认命名构造方法中,在源码中可以看到,默认命名其实就是Thread-X的命令(X为数字):

public Thread() {
    this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);
}

public Thread(Runnable target) {
    this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);
}

private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

而在命名构造方法就是自定义的名字。

另外,如果想修改线程的名字,可以调用setName()方法,但是需要注意,处于NEW状态的线程才能修改。

4.2 线程的父子关系

Thread的所有构造方法都会调用如下方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);

其中的一段源码截取如下:

if (name == null) {
    throw new NullPointerException("name cannot be null");
} else {
    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();
        }
    }
}

可以看到当前这里有一个局部变量叫parent,并且赋值为currentThread()currentThread()是一个native方法。因为一个线程被创建时的最初状态为NEW,因此currentThread()代表是创建自身线程的那个线程,也就是说,结论如下:

  • 一个线程的创建肯定是由另一个线程完成的
  • 被创建线程的父线程是创建它的线程

也就是自己创建的线程,父线程为main线程,而main线程由JVM创建。

另外,Thread的构造方法中有几个具有ThreadGroup参数,该参数指定了线程位于哪一个ThreadGroup,如果一个线程创建的时候没有指定ThreadGroup,那么将会和父线程同一个ThreadGroupmain线程所在的ThreadGroup称为main

4.3 关于stackSize

Thread构造方法中有一个stackSize参数,该参数指定了JVM分配线程栈的地址空间的字节数,对平台依赖性较高,在一些平台上:

  • 设置较大的值:可以使得线程内调用递归深度增加,降低StackOverflowError出现的概率
  • 设置较低的值:可以使得创建的线程数增多,可以推迟OutOfMemoryError出现的时间

但是,在一些平台上该参数不会起任何作用。另外,如果设置为0也不会起到任何作用。

5 Thread API

5.1 sleep()

sleep()有两个重载方法:

  • sleep(long mills)
  • sleep(long mills,int nanos)

但是在JDK1.5后,引入了TimeUnit,其中对sleep()方法提供了很好的封装,建议使用TimeUnit.XXXX.sleep()去代替Thread.sleep()

TimeUnit.SECONDS.sleep(1);
TimeUnit.MINUTES.sleep(3);

5.2 yield()

yield()属于一种启发式方法,提醒CPU调度器当前线程会自愿放弃资源,如果CPU资源不紧张,会忽略这种提醒。调用yield()方法会使当前线程从RUNNING变为RUNNABLE状态。

关于yield()sleep()的区别,区别如下:

  • sleep()会导致当前线程暂停指定的时间,没有CPU时间片的消耗
  • yield()只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,会导致线程上下文的切换
  • sleep()会使线程短暂阻塞,在给定时间内释放CPU资源
  • 如果yield()生效,yield()会使得从RUNNING状态进入RUNNABLE状态
  • sleep()会几乎百分百地完成给定时间的休眠,但是yield()的提示不一定能担保
  • 一个线程调用sleep()而另一个线程调用interrupt()会捕获到中断信号,而yield则不会

5.3 setPriority()

5.3.1 优先级介绍

线程与进程类似,也有自己的优先级,理论上来说,优先级越高的线程会有优先被调度的机会,但实际上并不是如此,设置优先级与yield()类似,也是一个提醒性质的操作:

  • 对于root用户,会提醒操作系统想要设置的优先级别,否则会被忽略
  • 如果CPU比较忙,设置优先级可能会获得更多的CPU时间片,但是空闲时优先级的高低几乎不会有任何作用

所以,设置优先级只是很大程度上让某个线程尽可能获得比较多的执行机会,也就是让线程自己尽可能被操作系统调度,而不是设置了高优先级就一定优先运行,或者说优先级高的线程比优先级低的线程就一定优先运行。

5.3.2 优先级源码分析

设置优先级直接调用setPriority()即可,OpenJDK 11源码如下:

public final void setPriority(int newPriority) {
    this.checkAccess();
    if (newPriority <= 10 && newPriority >= 1) {
        ThreadGroup g;
        if ((g = this.getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }

            this.setPriority0(this.priority = newPriority);
        }

    } else {
        throw new IllegalArgumentException();
    }
}

可以看到优先级处于[1,10]之间,而且不能设置为大于当前ThreadGroup的优先级,最后通过native方法setPriority0设置优先级。

一般情况下,不会对线程的优先级设置级别,默认情况下,线程的优先级为5,因为main线程的优先级为5,而且main为所有线程的父进程,因此默认情况下线程的优先级也是5。

5.4 interrupt()

interrupt()是一个重要的API,线程中断的API有如下三个:

  • void interrupt()
  • boolean isInterrupted()
  • static boolean interrupted()

下面对其逐一进行分析。

5.4.1 interrupt()

一些方法调用会使得当前线程进入阻塞状态,比如:

  • Object.wait()
  • Thread.sleep()
  • Thread.join()
  • Selector.wakeup()

而调用interrupt()可以打断阻塞,打断阻塞并不等于线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。一旦在阻塞状态下被打断,就会抛出一个InterruptedException的异常,这个异常就像一个信号一样通知当前线程被打断了,例子如下:

public static void main(String[] args) throws InterruptedException{
    Thread thread = new Thread(()->{
        try{
            TimeUnit.SECONDS.sleep(10);
        }catch (InterruptedException e){
            System.out.println("Thread is interrupted.");
        }
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();
}

会输出线程被中断的信息。

5.4.2 isInterrupted()

isInterrupted()可以判断当前线程是否被中断,仅仅是对interrupt()标识的一个判断,并不会影响标识发生任何改变(因为调用interrupt()的时候会设置内部的一个叫interrupt flag的标识),例子如下:

public static void main(String[] args) throws InterruptedException{
    Thread thread = new Thread(()->{
        while (true){}
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :"+thread.isInterrupted());
    thread.interrupt();
    System.out.println("Thread is interrupted :"+thread.isInterrupted());
}

输出结果为:

Thread is interrupted :false
Thread is interrupted :true

另一个例子如下:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    System.out.println("In catch block thread is interrupted :" + isInterrupted());
                }
            }
        }
    };
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :" + thread.isInterrupted());
    thread.interrupt();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :" + thread.isInterrupted());
}

输出结果:

Thread is interrupted :false
In catch block thread is interrupted :false
Thread is interrupted :false

一开始线程未被中断,结果为false,调用中断方法后,在循环体内捕获到了异常(信号),此时会Thread自身会擦除interrupt标识,将标识复位,因此捕获到异常后输出结果也为false

5.4.3 interrupted()

这是一个静态方法,调用该方法会擦除掉线程的interrupt标识,需要注意的是如果当前线程被打断了:

  • 第一次调用interrupted()会返回true,并且立即擦除掉interrupt标识
  • 第二次包括以后的调用永远都会返回false,除非在此期间线程又一次被打断

例子如下:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.interrupted());
            }
        }
    };
    thread.setDaemon(true);
    thread.start();
    TimeUnit.MILLISECONDS.sleep(2);
    thread.interrupt();
}

输出(截取一部分):

false
false
false
true
false
false
false

可以看到其中带有一个true,也就是interrupted()判断到了其被中断,此时会立即擦除中断标识,并且只有该次返回true,后面都是false

关于interrupted()isInterrupted()的区别,可以从源码(OpenJDK 11)知道:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return this.isInterrupted(false);
}

@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean var1);

实际上两者都是调用同一个native方法,其中的布尔变量表示是否擦除线程的interrupt标识:

  • true表示想要擦除,interrupted()就是这样做的
  • false表示不想擦除,isInterrupted()就是这样做的

5.5 join()

5.5.1 join()简介

join()sleep()一样,都是属于可以中断的方法,如果其他线程执行了对当前线程的interrupt操作,也会捕获到中断信号,并且擦除线程的interrupt标识,join()提供了三个API,分别如下:

  • void join()
  • void join(long millis,int nanos)
  • void join(long mills)

5.5.2 例子

一个简单的例子如下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList());
        threads.forEach(Thread::start);
        for (Thread thread:threads){
            thread.join();
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" # "+i);
            shortSleep();
        }
    }

    private static Thread create(int seq){
        return new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+" # "+i);
                shortSleep();
            }
        },String.valueOf(seq));
    }

    private static void shortSleep(){
        try{
            TimeUnit.MILLISECONDS.sleep(2);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

输出截取如下:

2 # 8
1 # 8
2 # 9
1 # 9
main # 0
main # 1
main # 2
main # 3
main # 4

线程1和线程2交替执行,而main线程会等到线程1和线程2执行完毕后再执行。

6 线程关闭

Thread中有一个过时的方法stop,可以用于关闭线程,但是存在的问题是有可能不会释放monitor的锁,因此不建议使用该方法关闭线程。线程的关闭可以分为三类:

  • 正常关闭
  • 异常退出
  • 假死

6.1 正常关闭

6.1.1 正常结束

线程运行结束后,就会正常退出,这是最普通的一种情况。

6.1.2 捕获信号关闭线程

通过捕获中断信号去关闭线程,例子如下:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(){
        @Override
        public void run() {
            System.out.println("work...");
            while(!isInterrupted()){

            }
            System.out.println("exit...");
        }
    };
    t.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("System will be shutdown.");
    t.interrupt();
}

一直检查interrupt标识是否设置为true,设置为true则跳出循环。另一种方式是使用sleep()

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(){
        @Override
        public void run() {
            System.out.println("work...");
            while(true){
                try{
                    TimeUnit.MILLISECONDS.sleep(1);
                }catch (InterruptedException e){
                    break;
                }
            }
            System.out.println("exit...");
        }
    };
    t.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("System will be shutdown.");
    t.interrupt();
}

6.1.3 volatile

由于interrupt标识很有可能被擦除,或者不会调用interrupt()方法,因此另一种方法是使用volatile修饰一个布尔变量,并不断循环判断:

public class Main {
    static class MyTask extends Thread{
        private volatile boolean closed = false;

        @Override
        public void run() {
            System.out.println("work...");
            while (!closed && !isInterrupted()){

            }
            System.out.println("exit...");
        }

        public void close(){
            this.closed = true;
            this.interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyTask t = new MyTask();
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("System will be shutdown.");
        t.close();
    }
}

6.2 异常退出

线程执行单元中是不允许抛出checked异常的,如果在线程运行过程中需要捕获checked异常并且判断是否还有运行下去的必要,可以将checked异常封装为unchecked异常,比如RuntimeException,抛出从而结束线程的生命周期。

6.3 假死

所谓假死就是虽然线程存在,但是却没有任何的外在表现,比如:

  • 没有日志输出
  • 不进行任何的作业

等等,虽然此时线程是存在的,但看起来跟死了一样,事实上是没有死的,出现这种情况,很大可能是因为线程出现了阻塞,或者两个线程争夺资源出现了死锁。

这种情况需要借助一些外部工具去判断,比如VisualVMjconsole等等,找出存在问题的线程以及当前的状态,并判断是哪个方法造成了阻塞。

以上是关于Java高并发学习笔记:Thread详解的主要内容,如果未能解决你的问题,请参考以下文章

并发编程(学习笔记-Java线程)-part2

Java高并发学习笔记:volatile关键字

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

Java并发编程Thread详解