一、线程简介
1、线程状态
线程在其生命周期内的所有状态如下表所示:
线程状态 | 状态说明 |
NEW | 初始状态,线程被构建,但还没有调用start()方法 |
RUNABLE | 运行状态,JAVA线程将操作系统中的就绪和运行两种状态笼统的称作“运行中”,即调用run()方法前后,统一都叫运行中 |
BLOCKED | 阻塞状态,表示线程阻塞与锁 |
WATING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做一些特定的动作(通知或中断) |
TIME_WATING | 超时等待,该状态类似于WATING,但是会在超时时间到时,如果还没有收到其他线程的特定动作,将自行返回 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程间间状态转换流程如下图所示:
由上图可见,当线程创建后,处于初始状态,调用start()方法后开始运行,进入运行状态,当线程执行wait()方法后,进入等待状态,如果其他线程执行notify()方法通知后,该线程状态重新变为运行中,而超时等待状态相当于在等待状态上加了超时时间设置;当线程调用同步方法时,在没有获取到锁的情况下,线程会进入阻塞状态,当线程获取到锁的时候,重新进入运行状态,最终线程执行完毕,进入终止状态。
说明:阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或者代码块时的状态,但是阻塞在java.consurrent包中Lock接口的线程状态却是等待状态,因为java.consirrent包中Lock接口阻塞的实现均使用了LockSupport类中的相关方法。
2、daemon线程
Deamon线程是一种支持线程,它主要被用作程序中后台调度以及支持工作。这意味着,当JVM中不存在非Deamon线程时,JVM将立即退出。立即二字用了红色标注,即只要不存在非守护线程(deamon线程),deamon线程如论是否执行完毕,都会退出,因此不能使用守护线程的finally代码块做资源控制操作,因为这个finally代码块不一定会执行。
可以调用Thread.setDeamon(true)将线程设置为deamon线程
3、线程中断
中断可以理解为线程的一个标识位,它表示一个运行中的线程是否被其他线程进行了中断操作,其他线程调用该线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来做相应的操作,线程可以使用isInterrupted()方法或静态方法Thread.interruped()方法来进行检查,两者的区别是,调用静态方法可以将中断标志复位,即如果线程A被线程B中断,那么调用多少次isInterrupted()方法返回值都是true(如果线程A执行完毕,那么此时返回false),但是调用A.interrupted()方法时,第一次为true,以后均为false,就是因为第一次调用后,已经将标志位复位。
从JAVA的API可以看出,很多方法声明抛出InterruptedException,这个异常在抛出前,会将中断状态复位,然后再抛出InterruptionException,那么此时调用isInterrupted()方法时,则返回false
4、线程暂停、恢复和停止(注:已经过期,不建议使用)
暂停(suspend())、恢复(resume())、停止(stop())如字面意思,对线程做相应的操作,但是已经是过期的方法,不建议使用。
不建议使用的原因是:
a、以suspend方法为例,在调用后,线程不会释放占有的资源,比如锁,线程是占有着资源进入等待状态,因此容易引发死锁问题。
b、stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给与线程释放资源的机会就直接停止了线程,因此会导致程序可能工作在不确定的状态下。
正是因为以上的副作用,suspent()、resume()、stop()才不建议使用,而暂停和恢复操作可以使用后续的等待/通知机制来代替。
那么对于停止线程呢,可以使用第3点中的interrupted进行优雅的线程中断。
@Slf4j public class ShutDown { public static void main(String[] args) throws Exception{ Runner a = new Runner(); Thread countThread = new Thread(a,"countThread-A"); countThread.start(); TimeUnit.SECONDS.sleep(1); countThread.interrupt();//重要 Runner b = new Runner(); Thread countThread2 = new Thread(b,"countThread-B"); countThread2.start(); TimeUnit.SECONDS.sleep(1); b.cancel(); } private static class Runner implements Runnable{ private long i; private volatile boolean on = true; @Override public void run(){ while (on && !Thread.currentThread().isInterrupted()){ i++; } log.info("{}-Count i = {}",Thread.currentThread().getName(),i); } public void cancel(){ on = false; } } }
标记“重要”的那行代码,为线程a设置了中断操作,在Runner中判断on和中断状态来取消累加操作,线程a使用的是中断来取消,线程b是调用cancel来取消。
5、等待通知机制
之前已经说明,对于同步代码块和同步方法都是首先要获得Object的监视器(monitor),在获取监视器前,有MonitorEnter指令,退出监视器时,是用MonitorExit指令。
对于此线程、对象、监视器和同步队列的关系如上图,一个线程要想获取一个同步代码块,首先是用MonitorEnter获取监视器Monitor,获取成功,则获取Object对象的锁,并进项相应操作,操作完毕后,通过MonitorExit指令释放锁,完成操作;如果在获取Minotor监视器时失败,则将该线程放入同步队列SychronizedQueue阻塞,待其他线程执行完MonitorExit操作后,该线程出同步队列,然后重新尝试获取监视器。
等待通知机制使用到的方法如下:
方法名称 | 描述 |
notify() | 通知一个在对象上等待的线程,使其从wait()方法中返回,而返回的线程获取到了对象的锁 |
nitifyAll() | 通知所有等待在该对象上的线程,重新争夺锁 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外的线程通知或被中断才会返回,需要注意,调用wait()方法后会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数是毫秒,也就是等待n毫秒,如果没有通知就返回超时 |
wait(long,int) | 对于超时时间更细粒度的控制,可以达到纳秒 |
代码示例:
@Slf4j public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] arg) throws Exception{ Thread waitThread = new Thread(new Wait(),"waitThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); Thread notifyThread = new Thread(new Notify(),"notifyThread"); notifyThread.start(); } static class Wait implements Runnable{ @Override public void run(){ synchronized (lock){ while (flag){ log.info("{} flag is true. wait @{}",Thread.currentThread(),LocalDateTime.now()); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info("{} flag is false. running @{}",Thread.currentThread(),LocalDateTime.now()); } } } static class Notify implements Runnable{ @Override public void run(){ synchronized (lock){ log.info("{} hold lock,notify @{}",Thread.currentThread(),LocalDateTime.now()); lock.notifyAll(); flag = false; try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (lock){ log.info("{} hold lock again,sleep @{}",Thread.currentThread(),LocalDateTime.now()); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
输出结果:
从输出可以看到,waitThread先获得了锁,做了输出,然后调用wait方法后,notifyThread线程获得锁,执行了notify操作,然后waitThread线程重新争夺锁,notifyThread线程也在第二次获取锁的时候进入锁的争夺,因此,输出的第三行和第四行不一定是按照上面截图的输出一样,有可能waiThread在notifyThread线程之前输出。
对于上面示例的等待通知机制,线程间转换如下图所示:
如上图所示,waitThread线程执行同步代码块,执行到wait()方法,释放对Monitor的持有,进入等待队列,线程状态变为WATING,notifyThread队列获取Monitor后,执行notify()或notifyAll()方法,waitThread线程从等待队列进入同步队列,线程状态变更为阻塞,此时notifyThread线程仍然只有Monitor,待notifyThread线程执行完MonitorExit指令后,释放对于Monitor的持有,然后waitThread线程出同步队列,重新竞争锁。
6、管道输入输出流
管道输入/输出流和普通的文件输入输出流或者网络的输入输出流不同在于,它主要用于线程间的数据传输,而传输的媒介为内存。
管道输入输出流主要提供了如下4种类:PipedOutPutStream、PipedInPutStream、PipedReader、PipedWriter
代码示例:
@Slf4j public class Piped { public static void main(String[] arg) throws Exception{ PipedWriter out = new PipedWriter(); PipedReader in = new PipedReader(); out.connect(in); Thread printThread = new Thread(new Print(in),"printThread"); printThread.start(); int receive = 0; try{ while ((receive=System.in.read())!=-1){ out.write(receive); } }catch (Exception e){ e.printStackTrace(); }finally { out.close(); } } static class Print implements Runnable{ private PipedReader in; public Print(PipedReader in){ this.in = in; } @Override public void run(){ int receive = 0; try{ while ((receive=in.read())!=-1){ System.out.print((char)receive); } }catch (Exception e){ e.printStackTrace(); } } } }
输出内容:
可以发现,输入的什么,就原样输出什么
7、Thread.join()
如果线程A执行了B.join(),表示当线程A等待线程B终止之后才会返回,才做后续操作,同时还提供了两个带有超时时间的方法join(long millis)和join(long millis,int nanos)。
代码示例:
@Slf4j public class Join { public static void main(String[] args) throws Exception{ Thread previous = Thread.currentThread(); for(int i=0;i<10;i++){ Thread thread = new Thread(new Domino(previous),String.valueOf(i)); thread.start(); previous = thread; } TimeUnit.SECONDS.sleep(5); log.info("{}执行完毕【{}】",Thread.currentThread().getName(),LocalDateTime.now()); } static class Domino implements Runnable{ private Thread thread; public Domino(Thread thread){ this.thread = thread; } @Override public void run(){ try { log.info("【{}】===join线程===【{}】===【{}】",thread.getName(),Thread.currentThread().getName(),LocalDateTime.now()); thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } log.info("【{}】执行完毕===【{}】",Thread.currentThread().getName(),LocalDateTime.now()); } } }
输出结果:
由上可见,join方法是乱序的,但是线程的执行顺序是有序的。
其实join也是等待通知的一种方式。
8、ThreadLocal
ThreadLocal设置的变量为线程变量,可以直接通过set一个值,然后后续再是用get获取到这个值,如以下代码所示:
@Slf4j public class Profiler { private static final ThreadLocalTIME_THREADLOCAL = new ThreadLocal (){ @Override protected Instant initialValue(){ return Instant.now(); } }; public static final void bagin(){ TIME_THREADLOCAL.set(Instant.now()); } public static final long end(){ return Duration.between(TIME_THREADLOCAL.get(),Instant.now()).toMillis(); } public static void main(String[] args) throws Exception{ Profiler.bagin();; TimeUnit.SECONDS.sleep(1); log.info("执行时间:{}",Profiler.end()); } }
输出结果:
17:07:09.327 [main] INFO com.example.jdk8demo.Profiler - 执行时间:1001
如上所示,可以用在统计方法调用耗时上,这样的好处是,调用begin和调用end可以不用在一个方法或者一个类种,比如是用AOP时。