并发03--并发编程基础

一、线程简介

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 ThreadLocal<Instant> TIME_THREADLOCAL = new ThreadLocal<Instant>(){
        @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时。

 

posted @ 2020-06-10 17:12  李聪龙  阅读(182)  评论(0编辑  收藏  举报