线程原理理解

1、多线程的实现

  *  继承Thread类创建线程,Thread本质上是实现了Runnable接口的一个实例。

  *  实现Runnable接口,主要避免java单继承问题,如果一个类已经extends另一个类,就无法再继承Thread,但可以实现Runnable接口来创建线程。

  *  实现Callable接口,带返回结果,可以通过Future来获取线程执行的结果。future.get()是阻塞的。

2、线程的生命周期

   从线程的创建到销毁,总共有6中状态:

  *  NEW:初始状态,线程被创建,但还没有调用start方法。

  *  RUNNABLE:可运行状态,java线程把操作系统中的就绪和运行两种状态统称为运行中。

  *  BLOCKED:阻塞状态,与I/O中的阻塞不同,这里特指被synchronized阻塞,一个正在阻塞等待一个监视器锁的线程处于这一状态。

  *  WAITING:一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态。处于这个状态是由于该线程调用了以下方法:

      ——不带限时的Object.wait方法;

      ——不带限时的Thread.join方法;

      ——LockSupport.park。

      ——线程执行IO操作,也会被放入等待队列。

    然后一直等待其他线程执行一个特别的动作,比如:

      ——一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 或 Object.notifyAll();

      ——一个调用了 Thread.join 方法的线程会等待指定的线程结束。

 

  *  TIMED_WAITING:限时等待状态,超时以后自动恢复。

      ——如Thread.sleep、带超时的wait和join等。

  *  TERMINATED:终止状态,表示当前线程执行完毕。

    

 3、线程启动原理

  调用start方法启动一个线程,然后执行完run方法后线程的生命周期也将终止。为什么调用start而不是直接调用run?

  调用start方法实际上是调用一个native方法start0( )来启动一个线程。start0( )是在Thread的静态代码块中注册的,

      

   registerNatives 的 本 地 方 法 的 定 义 在 文 件 Thread.c,Thread.c定义了各个操作系统平台要用的关于线 程的公共数据和操作。

      

   可以看出,start0( )会实际执行JVM_StartThread方法,最终会通过os::create_thread来调用平台创建线程的方法来创建线程,创建后通过os::start_thread来调用run方法。所以调用start方法包含了底层创建线程和启动线程两部分。

4、线程的终止

   停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出。

  通过thread.interrupt( )来设置中断标识位为true,线程中应该通过thread.isInterrupted( )来判断中断表示。

    

   也可以通过Thread.interrupted( )来恢复中断标识位,主要是为了通知中断其他线程已经收到中断通知了。InterruptedException异常在抛出前也会将中断标识位清空,然后再抛出异常。

 5、线程上下文切换

  多线程执行其实就是CPU分配时间片给每个线程,一个线程的时间片执行完了就换另一个拿到时间片的线程去执行,因为时间片执行很快,所以给我们感知是多个线程同时执行。线程间转换会发生上下文的切换,上下文切换是有一定开销的,所以并不是线程越多执行越快。理解上下文切换有助于我们更好地理解线程的执行过程。

   Linux根据权限将进行的运行空间分为内核态和用户态,内核态拥有最高权限,可以访问一切资源,用户态只能在用户空间运行。

  每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(CPU内的快速缓存,保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程),这些统称为线程状态数据。

  线程执行任务运行在用户态,切换到其他线程,就要先由用户态切换到内核态,内核态将上一个线程的状态数据保存到内存中,然后执行系统调度器的代码切换到另一个线程,这时要加载当前线程的状态数据,然后切换到用户态执行当前线程的任务。这个过程就是上下文切换。

 6、线程异常处理

   父线程无法感知子线程异常,如下:父线程中无法捕获子线程中的异常。

    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println(3 / 2);
            System.out.println(3 / 0);//发生异常
            System.out.println(3 / 1);
        };
        
        try {
            Thread thread = new Thread(task, "AAA");
            thread.start();
        } catch (Exception e) { // 抓不到
            System.out.println("errMsg=" + e.getMessage());
        }
    }

  那如何处理子线程中的异常呢?

方法一(不推荐):手动在每个run() 方法中进行try-catch

方法二(推荐):使用 UncaughtExceptionHandler

(1)给应用程序统一设置(作用域是整个应用)

  在spring框架中,可以使用@ExceptionHandler注解进行全局异常捕获。

public class MyUnCaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
​
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("捕获了:"+t.getName()+"线程的异常,exception="+e.getClass().getName()+",message="+e.getMessage());
    }
​
    public static void main(String[] args) throws InterruptedException {
        //设置全局异常捕获器
        Thread.setDefaultUncaughtExceptionHandler(new MyUnCaughtExceptionHandler());
​
        Runnable run = ()->{
            throw new RuntimeException();
        };
​
        new Thread(run,"线程1").start();
        Thread.sleep(300);
        new Thread(run,"线程2").start();
        Thread.sleep(300);
        new Thread(run,"线程3").start();
        Thread.sleep(300);
        new Thread(run,"线程4").start();
    }
}

执行结果:

捕获了:线程1线程的异常,exception=java.lang.RuntimeException,message=null
捕获了:线程2线程的异常,exception=java.lang.RuntimeException,message=null
捕获了:线程3线程的异常,exception=java.lang.RuntimeException,message=null
捕获了:线程4线程的异常,exception=java.lang.RuntimeException,message=null

(2)给单个线程设置(作用域是当前线程)

public class UseOneThread {
​
    public static void main(String[] args) {
        Runnable run = ()->{
            throw new ArithmeticException("分母不能为零");
        };
​
        Thread task = new Thread(run,"测试线程");
        //设置异常捕获器
        task.setUncaughtExceptionHandler(new MyUnCaughtExceptionHandler());
        task.start();
    }
}

执行结果:

捕获了:测试线程线程的异常,exception=java.lang.ArithmeticException,message=分母不能为零

(3)给线程池设置

public class UseExecutorTask {
​
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        /**
         * 使用Thread,线程池无法捕获其异常,需要将线程封装成 Runnable 或 Callable
         */
//        Thread task = new Thread(()->{
//            throw new RuntimeException("test exe");
//        });
//        task.setUncaughtExceptionHandler(new MyUnCaughtExceptionHandler());
​
        Runnable task = ()->{
            Thread.currentThread().setUncaughtExceptionHandler(new MyUnCaughtExceptionHandler());
            throw new RuntimeException("test exe");
        };
​
        executorService.execute(task);
        executorService.shutdown();
    }
}

  注意:需要将线程封装成 Runnable 或 Callable,抛出的异常才能被线程池捕获,同时还要注意线程池的 execute 和 submit 两种方式对异常的捕获也是不同的。

   

posted @ 2020-04-23 11:13  jingyi_up  阅读(100)  评论(0编辑  收藏  举报