【Java并发编程】- 01 并发编程基础

214 阅读14分钟
原文链接: mp.weixin.qq.com

线程创建

Thread & Runnable

ThreadRunnableJDK 1.0版本开始就出现的 APIThread类也实现了 Runnable接口:public class Thread implements Runnable

@Testpublic void test1(){	Runnable task = () -> System.out.println("time:"+System.currentTimeMillis());    new Thread(task).start();}

Thread 和 Runnable 区别:

Thread类本身也实现了 Runnable接口,从源码来看:继承Thread类然后重写run()和实现Runnable接口并传入Thread类在实现多线程的本质上并没有区别。区别只是 run()方法内容的来源:一种是重写run()方法;另一种是 Runnable传入

//Thread#runpublic void run() {    /*    target就是传入的Runnable,所以默认就是直接调用传入的Runnable,如果没有传入就空实现    所以,要么传入Runnable,要么重写Thread#run,否则就是空实现    */	if (target != null) {    	target.run();    }}

但是,在实际开发中建议采用Runnable方式:

  • 单继承Thread是类,存在 单点继承问题,Runnable是接口,就不存在这个问题;
  • 解耦Runnable接口中只定义了一个 run方法,封装的是用户的工作任务Runnable接口将 工作任务(run方法)和 线程创建/运行机制(Thread类)进行了解耦,也更加符合面向对象设计原则中 单一职责原则

Tips:Thread代表的线程是与底层操作系统中的线程对应的, Thread实例就是对底层操作系统中真实运行的线程映射,所以Thread类内部维护着大量与操作系统交互的 native方法,同时维护着线程各种状态信息,如:nameprioritydaemonstate等,Thread可以看着是对线程执行机制的封装;

  • 灵活性:由于工作任务执行机制的解耦,封装工作任务的Runnable可以传入到多个 Thread中,从而实现一个工作任务被多个线程同时处理;而继承Thread类方式创建线程,工作任务和线程绑定在一起,只能实现: 工作任务和线程一对一关系

Callable & Future

ThreadRunnable多线程编程模式存在的问题:任务执行完成之后无法获取返回结果。那如果我们想要获取返回结果该如何实现呢?从JDK 1.5开始引入了 CallableFuture,通过它们构建的线程,在任务执行完成后就可以获取执行结果。

@Testpublic void testCallable() throws ExecutionException, InterruptedException {	Callable<Long> task = () -> {    	TimeUnit.SECONDS.sleep(8);        return System.currentTimeMillis();    };    Future<Long> future = executors.submit(task);    //executors.shutdownNow();    executors.shutdown();    System.out.println("运行结果:"+future.get());}

Callable原理

1、ExecutorService内部 submit方法如下:

public <T> Future<T> submit(Callable<T> task) {    if (task == null) throw new NullPointerException();    RunnableFuture<T> ftask = newTaskFor(task);    execute(ftask);    return ftask;}protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {    return new FutureTask<T>(callable);}

源码分析:将Callable包装成 FutureTask对象,FutureTask类实现 RunnableFuture接口,该接口又继承Runnable接口: public interface RunnableFuture<V> extends Runnable, Future<V>,即间接的将Callable对象封装成一个 Runnable对象。

FutureTask#run方法内部调用 Callable#call方法,并将返回结果保存到全局变量outcome中,同时 Future接口中定义的方法可以将该值暴露出去,大致逻辑见下图:

FutureTask有那么一点生产者/消费者概念, Runnable就像一个生产者,执行任务并将结果放入到变量outcome中; Future就像一个消费者,待生产完成,再从outcome中获取结果:

总结

在实际开发中更多关注的是业务逻辑,而很少去关注线程执行机制,更很少去修改,所以一般采用解耦方式;再一个,实际开发中更多采用线程池创建、管理线程,工作任务一般封装到RunnableCallable接口中,提交到线程池中运行。

守护线程

Java中的线程分为两类: daemon线程(守护线程)和user 线程(用户线程)。区别之一是当最后一个非守护线程结束时, JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM的退出。言外之意,只要有一个用户线程还没结束, 正常情况下JVM就不会退出。

守护线程在非守护线程全部结束后直接结束,即finally语句块中语句也不保证一定会被执行。如下,如果 thread线程是守护线程,当所有非守护线程运行完成,thread线程直接退出, finally语句块也不一定会被执行到。

案例一:

Thread thread = new Thread(){	@Override    public void run() {    	try {        	while(!isInterrupted()){            	try {                	TimeUnit.SECONDS.sleep(1);                } catch (InterruptedException e) {                     System.out.println(getName()+" interrupt flag is "+isInterrupted());                     interrupt();                     e.printStackTrace();                }             }         }finally {             System.out.println("finally语句块被执行");         }    }};

案例二:

public static void main(String[] args) throws InterruptedException {    Thread thread = new Thread(() -> {        while (true) {            System.out.println("time:" + System.currentTimeMillis());            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    });    thread.start();    System.out.println("begin sleep");    TimeUnit.SECONDS.sleep(60);    System.out.println("main thread over");}

上面两张图对比发现:main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程, 该线程会等待所有用户线程结束后终止JVM进程

主线程结束后并不影响用户线程执行,即主线程结束后用户线程和守护线程依然可以继续执行,当所有用户线程结束后JVM就会退出,这时守护线程也会立即结束。

注意:

1、使用Junit测试用例时,如果主线程退出,用户线程和守护线程也会立即结束,程序退出;

2、另外还需要注意:如果在线程t1中创建一个守护 线程t2线程t1执行完成也不会影响守护 线程t2的执行;

3、守护线程中的try...finally,其中 finally语句块不一定会被执行;

线程生命周期

通用的线程生命周期可以通过五种状态来描述:初始状态、可运行状态、运行状态、阻塞状态和终止状态。具体描述见下:

  1. 初始状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于初始状态。
  2. 可运行/就绪状态:当调用了线程对象的start方法之后,该线程就进入了可运行状态;或者在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态;处于该状态的线程表示:一切准备工作都已完成,只需等待线程调度程序给该线程分配 CPU时间片资源执行任务。
  3. 运行状态:当有空闲的CPU时间片资源时(操作系统任务调度),操作系统就会将其分配给一个处于可运行状态的线程,被分配到 CPU时间片的线程的状态就转成运行状态,此时开始真正运行run方法中的用户业务逻辑代码。
  4. 阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行,sleepjoinwait等方法都可以导致线程阻塞。
  5. 终止/死亡状态:如果一个线程的run方法执行结束,对于已经终止的线程,无法再使用 start方法令其进入可运行状态。

上面是在各种语言中都较为通用的线程声明周期模型描述,Java语言则根据自身实现对其进行了简化和细化:

  • 简化:可运行状态到运行状态是由操作系统层面调度使用,JVM层面不关心,也没法控制这两个状态,所以合并成一个状态统称 可运行状态
  • 细化:阻塞状态进行了细化:BLOCKEDWAITINGTIMED_WAITING

Java线程的六种状态可参加Thread.State

  1. NEW:初始状态,线程被构建,但是还没有调用 start()方法
  2. RUNNABLE:运行状态,Java线程把操作系统中就绪和运行两种状态统称为 运行中
  3. BLOCKED:阻塞状态,线程等待 synchronized隐式锁时处于此状态,当获取到synchronized隐式锁后,又会从 BLOCKED转换到RUNNABLE状态。
  4. WAITING:无限期等待另一个线程来执行某一特定操作的线程处于这种状态。
  5. TIMED_WAITING:有限期/超时等待状态,超时以后自动返回。
  6. TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。

BLOCKEDWAITINGTIMED_WAITING处于这三种状态之一,即使 CPU资源空闲依然没有CPU的使用权。

阻塞API

1、等待synchronized隐式锁的线程状态: BLOCKED

2、阻塞集合如:ArrayBlockingQueueLinkedBlockingQueue,编程式锁:ReentrantLockReentrantReadWriteLock等阻塞底层都是通过LockSupport.park()实现,而 LockSupport.park()方法又调用sun.misc.Unsafe.park(),该方法定义如下: public native void park(boolean isAbsolute, long time),该种方式导致的线程阻塞线程状态为:WAITINGvoid parkNanos(Object blocker, long nanos)void parkUntil(Object blocker, long deadline)带超时参数阻塞的线程状态是: TIMED_WAITING

public static void main(String[] args) {    System.out.println(ManagementFactory.getRuntimeMXBean().getName());    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(10);    Thread t = new Thread("simon-thread"){      @Override      public void run() {        System.out.println("begin get");        String ele = null;        try {          //Queue为空导致阻塞          ele = queue.take();        } catch (InterruptedException e) {          e.printStackTrace();        }        System.out.println("ele:"+ele);      }    };    t.start();}

查看线程栈:

3、Thread.join()底层通过调用 Object.wait()方法实现,而Thread.join(long millis)调用的是 native void wait(long timeout)wait方法有两类: void wait()native void wait(long timeout)wait()阻塞的线程状态是WAITTING,而 wait(long timeout)阻塞的线程状态是TIMED_WAITING

  public static void main(String[] args) throws InterruptedException {    System.out.println(ManagementFactory.getRuntimeMXBean().getName());    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(10);    Thread t = new Thread("simon-thread"){      @Override      public void run() {        System.out.println("begin get");        String ele = null;        try {          ele = queue.take();        } catch (InterruptedException e) {          e.printStackTrace();        }        System.out.println("ele:"+ele);      }    };    t.start();    //调用join方法阻塞主线程    t.join();  }

查看线程栈:

4、Thread类的 sleep方法最终调用的是Thread类中 nativesleep方法, sleep阻塞的线程状态:TIMED_EAITING

  public static void main(String[] args) throws InterruptedException {    System.out.println(ManagementFactory.getRuntimeMXBean().getName());    Thread t = new Thread("simon-thread"){      @Override      public void run() {        System.out.println("begin get");        try {          //调用Thread.sleep()方法使线程休眠          TimeUnit.SECONDS.sleep(100);        } catch (InterruptedException e) {          e.printStackTrace();        }      }    };    t.start();  }

查看线程栈:

5、Thread.yield()不会释放锁,调用 yield方法线程会进入可运行状态:RUNNABLE

6、其它操作系统层的阻塞,如IO阻塞ServerSocket.accept()

public static void main(String[] args) {    System.out.println(ManagementFactory.getRuntimeMXBean().getName());    Thread t = new Thread("simon-thread"){      @Override      public void run() {        int i = 0;        while(i != -1){          try {            //等待IO输入阻塞            i = System.in.read();          } catch (IOException e) {            e.printStackTrace();          }          System.out.println(Thread.currentThread().getName()+":"+i);        }      }    };    t.start();}

查看线程栈:

线程终止

方式一:Thread API方式

    private Object lock = new Object();    private Integer v1 = 100;    private Integer v2 = 100;    /**     * 1、stop()会立即抛出一个ThreadDeath类型的Error,不管线程内部有没有阻塞方法;     * 2、如果线程持有某个对象锁,调用stop()终止线程后锁会被释放;     * 3、stop()容易导致状态不一致问题,如下案例,更新状态1后由于调用stop()方法导致更新状态2步骤并没有被执行,其它     * 线程访问时就会出现状态不一致问题     * */    @Test    public void testStop() throws InterruptedException {        Thread t1 = new Thread("t1") {            @Override            public void run() {                synchronized (lock){                    System.out.println(Thread.currentThread().getName()+" 获取到锁");                    try {                        v1 += 10;//更新状态1                        TimeUnit.SECONDS.sleep(10);//模拟耗时操作                        v2 -= 10;//更新状态2                    } catch (InterruptedException e) {                        System.out.println(Thread.currentThread().getName()+" receive InterruptedException");                    } catch (Throwable e){//ThreadDeath                        System.out.println(Thread.currentThread().getName()+" receive exception:"+e);                    }                }            }        };        Thread t2 = new Thread("t2") {            @Override            public void run() {                synchronized (lock){                    System.out.println(Thread.currentThread().getName()+" 获取到锁");                    System.out.println("v1="+v1+",v2="+v2);                }            }        };        t1.start();        t2.start();        TimeUnit.SECONDS.sleep(2);        System.out.println("t1 stop");        t1.stop();        t1.join();        t2.join();        System.out.println("****finish***");    }

stop()是一个被废弃的方法,容易导致状态不一致问题,所以在生产开发中基本不会使用。

Thread类还提供了一个 destroy()方法,但是该方法一直没有被实现过,也被标注为废弃方法:

@Deprecatedpublic void destroy() {    throw new NoSuchMethodError();}

方式二:interrupt

interrupt本质上不会进行线程的终止操作的,它只是利用线程的中断标志位进行通知,是否真正终止线程以及何时终止都是由当前线程自己决定。主要分为三种情况:

  • 如果目标线程在调用Objectwait()wait(timeout)join()join(timeout)sleep(timeout)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出 InterruptedException异常;

  • 如果目标线程是被I/O或者 Nio中的Channel所阻塞,如 InterruptibleChannel相关方法、Selector相关方法等,同样 I/O操作会被中断或者返回特殊异常值,达到终止线程目的;

  • 如果以上条件都不满足,即不含有阻塞方法,则会设置此线程的中断状态。

例子1:循环中没有阻塞方法,则可以在每次循环中根据中断状态判断是否结束:

public void testInterruptStop() throws InterruptedException {    Thread t1 = new Thread("t1") {        @Override        public void run() {            /**              * interrupted():static方法,等价于Thread.currentThread().isInterrupted(),但是它会清除中断状态              * Thread.currentThread().isInterrupted():不会清除中断状态,              */            while(!Thread.interrupted()){//根据中断状态判断是否结束当前线程                System.out.println("执行");            }        }    };    t1.start();    TimeUnit.SECONDS.sleep(5);    t1.interrupt();    t1.join();    System.out.println("****finish***");}

例子2:循环中含有阻塞方法时,会抛出InterruptedException异常,但是中断状态会被清除:

public void testInterruptStop() throws InterruptedException {    Thread t1 = new Thread("t1") {        @Override        public void run() {            /**              * interrupted():static方法,等价于Thread.currentThread().isInterrupted(),但是它会清除中断状态              * Thread.currentThread().isInterrupted():不会清除中断状态,              */            while(!Thread.interrupted()){//判断标志位是否终止线程                System.out.println("执行");                try {                    TimeUnit.SECONDS.sleep(1);//添加阻塞方法                } catch (InterruptedException e) {//阻塞方法通过抛出InterruptedException异常方式响应中断                    e.printStackTrace();                    //注意,如果这里通过isInterrupted()获取中断状态返回false                    Thread.currentThread().interrupt();                }            }        }    };    t1.start();    TimeUnit.SECONDS.sleep(5);    t1.interrupt();    t1.join();    System.out.println("****finish***");}

Thread下几个相关中断方法区别:

interrupted()static方法,等价于Thread.currentThread().isInterrupted(),但是它会清除中断状态,这个一定要注意是当前线程中断状态,与哪个 Thread实例调用没有关系,如t1.interrupted()不是返回 t1线程中断状态;

isInterrupted():返回调用的 Thread实例的中断状态,如t1.interrupted()则表示返回 t1线程中断状态,同时不会清除中断状态;

interrupt():触发中断;

InterruptedException异常说明:

  • 如果一个方法声明抛出InterruptedException,则该方法是阻塞方法;
  • 程序中捕获到InterruptedException异常时,不要 e.printStackTrace()或通过日志记录log.error("exception", e)等方式进行简单处理,这样会导致无意间中断信号被屏蔽而得不到正常的处理。 InterruptedException异常表示接收到终止当前线程的信号,如果当前位置可以终止线程则直接进行处理;否则则需要让更上层程序感知中断发生,一般有两种处理方式:
    • 传递中断:在方法声明时添加throws InterruptedException
    • 恢复中断:如果try...catch捕获到 InterruptedException,但是自己又没法处理,外层代码有中断标志位检测,这时就可以通过重新触发一次中断Thread.currentThread().interrupt(),让外层的代码能够感知到中断发生过。

方式三:标志变量

在线程循环逻辑中添加一个volatile类型标志变量判断,用来控制线程执行终止:

private volatile boolean canceled = false;@Testpublic void testFlagStop() throws InterruptedException {    Thread t1 = new Thread("t1") {        @Override        public void run() {            while(!canceled){//判断标志位是否终止线程                System.out.println("执行");                //位置1            }        }    };    t1.start();    TimeUnit.SECONDS.sleep(5);    canceled = true;    t1.join();    System.out.println("****finish***");}

自定义标志变量方式控制线程终止,注意的一种情况是:循环中存在长时间阻塞方法,比如在位置1处添加 TimeUnit.SECONDS.sleep(1000),即使把canceled设置成 true,由于阻塞导致一直无法运行到循环判断处,线程也就没法终止。

所以,优先采用interrupt通知机制,这种方式支持的更加广泛,或者 interrupt和标志变量一起:while(!cancel && !Thread.interrupted())

总结

实际开发中,更多采用Interrupt通知机制实现线程的终止,而不是强制性终止线程。因为,发出停止信号的线程很可能对将要被终止的线程运行逻辑并不很熟悉,大多数情况下往往希望被终止线程完成一些列保存工作或工作交接后再停止,而不是立即终止线程导致数据处于一种混乱状态,产生数据不一致问题。

线程异常

Java 1.5出现的 UncaughtExceptionHandler,用于当线程由于未捕获异常终止时回调接口。

首先来看个例子:

@Slf4jpublic class ThreadException implements Runnable{    @Override    public void run() {        //子线程中直接抛出异常        throw new RuntimeException();    }    public static void main(String[] args) {        ExecutorService executors = Executors.newCachedThreadPool();        try {            //子线程中抛出异常,外部线程是无法try...catch捕获到            executors.execute(new ThreadException());            log.info("主线程继续执行");        }catch (Exception e){            log.error("捕获到异常", e);        }    }}

输出结果:

Exception in thread "pool-1-thread-1" java.lang.RuntimeException	at org.simon.core.basic.uncaughtexception.ThreadException.run(ThreadException.java:19)	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)	at java.lang.Thread.run(Thread.java:748)10:50:39.942 [main] INFO org.simon.core.basic.uncaughtexception.ThreadException - 主线程继续执行

从输出结果可以发现,子线程中抛出异常时主线程通过try...catch捕获不到异常信息,可以得出一个结论:线程的异常是逃逸不出该线程环境的,当线程中抛出异常且未进行处理会导致当前线程终结,而对主线程和其它线程完全不受影响,且完全感知不到某个线程抛出的异常

JVM的这种设计源自于一种理念:线程是独立执行的代码片段,线程的问题应该由线程自己来解决,而不要委托到外部。基于这样的设计理念,在 Java中,线程中的异常不论是checked exception还是 unchecked exception都应该在run()方法边界进行 try...catch捕获并处理掉。

一旦一个线程抛出了非受检异常,JVM就会把它杀死,在临近销毁之前, JVM会使用Thread.getUncaughtExceptionHandler()方法查看该线程上的 UncaughtExceptionHandler,并回调 uncaughtException()方法。

@Slf4jpublic class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{    /**     * UncaughtExceptionHandler#uncaughtException会在线程因未捕获异常而临近死亡时被调用     * @param t     * @param e     */    @Override    public void uncaughtException(Thread t, Throwable e) {        log.error("捕获到线程异常, thread name:{}, thread state:{}", t.getName(), t.getState(), e);    }    public static void main(String[] args) {        log.info("application:{}", ManagementFactory.getRuntimeMXBean().getName());        //设置DefaultUncaughtExceptionHandler,应用于所有的Thread        //如果只是指定某些线程,可以使用setUncaughtExceptionHandler        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler());        //创建线程池        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,                10, TimeUnit.SECONDS,                new ArrayBlockingQueue(10),                new ThreadFactoryBuilder().setNameFormat("executor-%d").build(),                new ThreadPoolExecutor.DiscardPolicy());        try {            executor.execute(new ThreadException());            log.info("主线程继续执行");        } catch (Exception e) {            log.error("捕获到异常", e);        }    }}

输出结果:

11:32:16.323 [main] INFO org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - application:25160@PS20190709MKED11:32:16.331 [main] INFO org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - 主线程继续执行11:32:16.333 [executor-0] ERROR org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - 捕获到线程异常, thread name:executor-0, thread state:RUNNABLEjava.lang.RuntimeException: null	at org.simon.core.basic.uncaughtexception.ThreadException.run(ThreadException.java:18)	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)	at java.lang.Thread.run(Thread.java:748)

线程executor-0执行时抛出异常未被捕获而被终止,在线程临近死亡前会回调 UncaughtExceptionHandler#uncaughtException

如上图,通过arthas工具查看线程状态,可以看到 线程executor-0已被销毁掉,线程池创建了一个新线程 executor-1来代替该销毁的线程。