zoukankan      html  css  js  c++  java
  • 并发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时。

  • 相关阅读:
    pandas的简单使用
    java搭建web从0-1(第一步:创建web工程)
    android通过命令行安装sdk
    iOS8不能通过itms-services协议下载安装app
    date命令转换日期命令提示date: illegal time format
    mac通过命令行获取证书和配置文件过期时间
    jenkin 不必要的Execute shell执行失败,导致jenkins都失败的解决
    命令行执行jenkins,构建job(可传递参数)
    Eclipse启动分析。。
    java非web应用修改 properties/xml配置文件后,无需重启应用即可生效---自动加载
  • 原文地址:https://www.cnblogs.com/liconglong/p/13086671.html
Copyright © 2011-2022 走看看