乐趣区

关于java: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 等等,找出存在问题的线程以及以后的状态,并判断是哪个办法造成了阻塞。

退出移动版