【Java并发编程】了解线程属性,如何处理子线程异常

苡仁 2020年04月12日 105次浏览

1. 线程有哪些常见属性?

  • 线程ID:线程用ID来标识出不同线程
  • 线程名字(Name):让用户或者程序猿开发调试或运行中定位线程的问题等。
  • 守护线程(isDaemon):当为true时,代表该线程为守护线程,false为非守护线程,也可以称作用户线程
  • 线程优先级(Priority):作用是告诉线程调度器,希望那个线程多运行,那个线程少运行。

1.1 线程ID

  • 线程ID从1开始,JVM运行起来后,我们自己创建的线程Id 早已不是0
/**
 * @author yiren
 */
public class ID {
    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());
        System.out.println(thread.getName() + ": " + thread.getId());
    }
}
main: 1
Thread-0: 11

Process finished with exit code 0
  • 在线程初始化中,有tid赋值
    /* For generating thread ID */
    private static long threadSeqNumber;

		private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
				......
        /* Set thread ID */
        tid = nextThreadID();
    }
    
    
    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }
  • threadSeqNumber没有赋值,所以为0,而nextThreadID()++threadSeqNumber使得线程从ID从1开始.

  • 那么为什么答应出来的子线程ID是11呢?

    • 如果你是用的IDEA,可以通过debug下个断点在最后一句上面,然后看一下当前线程的情况。
    • 实际就是JVM帮我们起了一些线程。

1.2 线程名字

  1. 默认线程名字源码分析:

    • 在没有指定线程名字的时候,线程默认传一个"Thread-" + nextThreadNum()作为名字
    • 而nextThreadNum获取到的threadInitNumber则是一个从0自增的一个静态变量。因为用了synchronized,所以是不会重复的。
    		/* For autonumbering anonymous threads. */
        private static int threadInitNumber;
        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        } 
    		public Thread() {
            init(null, null, "Thread-" + nextThreadNum(), 0);
        }
    
  2. 如何修改线程名字

    • 一个是通过构造方法
    • 另一个是通过setName(String name)设置
    		public Thread(String name) {
            init(null, null, name, 0);
        }
        private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize) {
            init(g, target, name, stackSize, null, true);
        }
        private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc,
                          boolean inheritThreadLocals) {
            if (name == null) {
                throw new NullPointerException("name cannot be null");
            }
    
            this.name = name;
    				...
        }
    
        public final synchronized void setName(String name) {
            checkAccess();
            if (name == null) {
                throw new NullPointerException("name cannot be null");
            }
    
            this.name = name;
            if (threadStatus != 0) {
                setNativeName(name);
            }
        }
    
    • 注意setName(String name)调用的时候,当线程是NEW状态的时候,设置name会一同把JVM里面的CPP的线程名字设置好,而如果不是NEW状态则只能修改java中我们可以看到的名称。

1.3 守护线程(Daemon)

  • 作用:给用户线程提供服务

  • 三个特性:

    • 线程默认类型继承自父线程
    • 被谁启动
    • 不影响JVM退出
  • 守护线程和普通线程的区别

    • 整体没有太大区别
    • 唯一的区别是是否影响JVM的退出
  • 常见面试问题

    • 守护线程和用户线程的区别
    • 我们是否需要给线程设置为守护线程?
      • 不需要,如果设置生守护线程,在JVM退出时会忽略你正在执行的任务,如果你正在执行一些数据操作,那么就会造成数据不一致了。

1.4 线程优先级

  • 在Java中优先级有个10个等级,默认为5,通过setPriority(int newPriority)设置
  • 但是我们程序在编码的时候,不应该依赖于优先级
    • 高度依赖于操作系统,不同的操作系统在实现优先级执行的时候不一样(windows中只有7个等级,更甚有的没有优先级。)设置优先级是不可靠的
    • 优先级可能会被操作系统改变
		/**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
    
    public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

1.5 面试题

  1. 什么时候我们需要设置守护线程?

  2. 我们应该如何应用线程优先级来帮助程序运行?有哪些禁忌?

  3. 不同的操作系统如何处理优先级问题?

2. 未捕获异常处理UncaughtException

2.1 使用UncaughtExceptionHandler处理

  1. 为什么要使用UncaughtExceptionHandler来处理?

    • 主线程可以轻松发现异常,而子线程却不行
    /**
     * 单线程抛出处理有异常堆栈
     * 而多线程,子线程发生异常有什么不同?
     *
     * @author yiren
     */
    public class ExceptionInChild {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                throw new RuntimeException();
            });
            thread.start();
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " +i);
            }
        }
    }
    
    Exception in thread "Thread-0" java.lang.RuntimeException
    	at com.imyiren.concurrency.thread.uncaughtexception.ExceptionInChild.lambda$main$0(ExceptionInChild.java:14)
    	at java.lang.Thread.run(Thread.java:748)
    main: 0
    main: 1
    main: 2
    main: 3
    main: 4
    
    Process finished with exit code 0
    
    • 由上可看出,子线程报错,丝毫不印象主线程的执行。
    • 子线程的异常无法用传统的方法捕获
    /**
     * 1. 不加try-catch 抛出四个异常
     * 2. 加了try-catch 期望捕获第一个线程的异常,线程234应该不运行,希望看到CaughtException
     * 3. 执行时发现,根本没有CaughtException,线程234依旧运行并抛出异常
     *
     * @author yiren
     */
    public class CantCatchDirectly {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                throw new RuntimeException();};
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            Thread thread3 = new Thread(runnable);
            Thread thread4 = new Thread(runnable);
    
            try {
                thread1.start();
                Thread.sleep(200);
                thread2.start();
                Thread.sleep(200);
                thread3.start();
                Thread.sleep(200);
                thread4.start();
            } catch (RuntimeException e) {
                System.out.println("caught exception");
            }
        }
    }
    
    Exception in thread "Thread-0" java.lang.RuntimeException
    	at com.imyiren.concurrency.thread.uncaughtexception.CantCatchDirectly.lambda$main$0(CantCatchDirectly.java:15)
    	at java.lang.Thread.run(Thread.java:748)
    Exception in thread "Thread-1" java.lang.RuntimeException
    	at com.imyiren.concurrency.thread.uncaughtexception.CantCatchDirectly.lambda$main$0(CantCatchDirectly.java:15)
    	at java.lang.Thread.run(Thread.java:748)
    Exception in thread "Thread-2" java.lang.RuntimeException
    	at com.imyiren.concurrency.thread.uncaughtexception.CantCatchDirectly.lambda$main$0(CantCatchDirectly.java:15)
    	at java.lang.Thread.run(Thread.java:748)
    Exception in thread "Thread-3" java.lang.RuntimeException
    	at com.imyiren.concurrency.thread.uncaughtexception.CantCatchDirectly.lambda$main$0(CantCatchDirectly.java:15)
    	at java.lang.Thread.run(Thread.java:748)
    
    Process finished with exit code 0
    
    • 如上,无法用传统的方法来捕获异常信息。
    • try-catch是针对主线程的,而不是针对子线程的。throw new RuntimeException()是运行在子线程的。
  2. 解决上面的主线程无法捕获的问题:

  • 方案一(不推荐):在run()方法中进行try-catch

    /**
     * 方案一:在run方法中try-catch
     * @author yiren
     */
    public class CatchExceptionInRun {
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    throw new RuntimeException();
                } catch (RuntimeException e) {
                    System.out.println("Caught Exception ...");
                }
            }).start();
        }
    }
    
    Caught Exception ...
    
    Process finished with exit code 0
    
  • 方案二:利用UncaughtExceptionHandler接口处理

    • 先看下线程异常处理器的调用策略 在ThreadGroup类中
    • 它会检查是否有父线程,如果父线程不为空就一直向上找到最顶层。
    • 如果没有,那就尝试获取默认的异常处理器。如果取到的实现不为空,那就调用实现的处理方式,如果为空那就打印异常堆栈信息。
    • 从上面的案例可知 没有实现的时候是直接打印异常堆栈。
        public void uncaughtException(Thread t, Throwable e) {
            if (parent != null) {
                parent.uncaughtException(t, e);
            } else {
                Thread.UncaughtExceptionHandler ueh =
                    Thread.getDefaultUncaughtExceptionHandler();
                if (ueh != null) {
                    ueh.uncaughtException(t, e);
                } else if (!(e instanceof ThreadDeath)) {
                    System.err.print("Exception in thread \""
                                     + t.getName() + "\" ");
                    e.printStackTrace(System.err);
                }
            }
        }
    
    1. 给程序统一设置

      • 首先自定义一个Handler
      /**
       * 自定义异常Handler
       * @author yiren
       */
      public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
      
          private String name;
      
          public MyUncaughtExceptionHandler(String name) {
              this.name = name;
          }
      
          @Override
          public void uncaughtException(Thread t, Throwable e) {
              Logger logger = Logger.getAnonymousLogger();
              logger.log(Level.WARNING,  name + "caught thread exception : " + t.getName());
          }
      }
      
      • 然后设置默认处理器
      /**
       * 使用自定义的handler
       * @author yiren
       */
      public class CatchByOwnUncaughtExceptionHandler {
          public static void main(String[] args) throws InterruptedException {
              Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("catch-handler"));
      
              Runnable runnable = () -> {
                  throw new RuntimeException();
              };
      
              Thread thread1 = new Thread(runnable);
              Thread thread2 = new Thread(runnable);
      
              thread1.start();
              Thread.sleep(200);
              thread2.start();
          }
      }
      
      二月 12, 2020 2:09:20 下午 com.imyiren.concurrency.thread.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
      警告: catch-handlercaught thread exception : Thread-0
      二月 12, 2020 2:09:20 下午 com.imyiren.concurrency.thread.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
      警告: catch-handlercaught thread exception : Thread-1
      二月 12, 2020 2:09:21 下午 com.imyiren.concurrency.thread.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
      警告: catch-handlercaught thread exception : Thread-2
      二月 12, 2020 2:09:21 下午 com.imyiren.concurrency.thread.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
      警告: catch-handlercaught thread exception : Thread-3
      
      Process finished with exit code 0
      
      • 如上可以看到 线程异常处理是使用我们自定义的处理器。
    2. 可以给每个线程单独设置

      • 可以通过thread.setUncaughtExceptionHandler(handler)设置
    3. 给线程池设置

      • 可以通过ThreadPoolExecutor来处理

2.2 面试题

  1. Java异常体系

  2. 实际工作中,如何处理全局异常?为什么要处理全局异常?不处理行不行?

  3. run方法是否可以抛出异常?如果抛出异常,线程状态会怎么样?

  4. 线程中如何处理某个未处理异常?


  • 文章内容来源:
  • 《Java多线程编程核心技术》、JDK1.8版本源码、慕课网悟空JUC课程