线程组和未处理的异常
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,对线程组的控制相当于同时控制这批线程.用户所创建的所有线程都属于指定线程组,若没有显示指定线程组,则属于默认线程组.默认情况下子线程和创建它的副线程处于同一个线程组内;例如:A创建了B,则默认A和B处于同一个线程组.
一但加入某个线程组,则中途不能改变该线程的线程组,直到该线程死亡.
Thread提供以下几种方式创建新的线程
- Thread(ThreadGroup group, Runnable target) 以target的run作为线程体创建新的线程,属于group组
- Thread(ThreadGroup group, Runnable target, String name) 以target的run作为线程体创建新的线程,属于group组,名字为name
- Thread(ThreadGroup group, String name) 创建新的线程,属于group组
因为中途不可改变所属线程组,所以没有setThreadGroup()方法,但可以通过getThreadGroup()来返回所属ThreadGroup对象.ThreadGroup类有如下两个构造器
- ThreadGroup(String name) 以指定的名字创建新的线程组
- ThreadGroup(ThreadGroup parent, String name) 以指定的名字,父线程组来创建新的线程组
ThreadGroup提供了以下几种常用的方法来管理线程组内的所有线程
- int activeCount(): 返回此线程组中活动线程的数目
- interrupt(): 中断此线程组中的所有线程
- isDaemon(): 判断该线程组是否是后台线程
- setDaemon(boolean daemon): 这是是否后台线程
- setMaxPriority(int pri): 设置线程组的最高优先级
接下来就其中一些方法进行使用演示
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
运行结果:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
ThreadGroup内还定义了一个比较有用的方法:void uncaughtException(Thread t,Throwable e),该方法可以处理该线程组内的线程所抛出的未处理异常.
从JDK1.5开始,Java加强了线程的异常处理,如果执行过程中抛出了一个未处理了异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常.
Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口内只有一个方法:void uncaughtException(Thread t,Throwable e),方法中t代表出现异常的线程,而e代表该线程抛出的异常.
Thread类中提供了两个方法来设置异常处理器:
- static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh): 为该线程类的所有线程实例设置默认的异常处理器
- void setUncaughtExceptionHandler(UncaughtExceptionHandler eh): 为指定的线程实例设置异常处理器
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器.当一个线程抛出未处理异常时,JVM会首先查找该线程对应的异常处理器(setDefaultUncaughtExceptionHandler方法设置的异常处理器),如果找到该异常处理器,将调用该异常处理器处理异常.否则,JVM将会调用该线程所属的线程组对象的uncaughtException方法来处理该异常.
我们来看一下ThreadGroup里是如何实现Thread.UncaughtExceptionHandler里的接口uncaughtException的
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 如果该线程组有父线程,则调用父线程组的uncaughtException方法来处理该异常.
- 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器,此异常处理器是为该线程所有实例设置的),那就调用该处理器来处理器处理该异常.
- 如果该异常对象是ThreadDeath对象,将不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程.
接下来我们自己实现Thread.UncaughtExceptionHandler进行演示
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
在主线程进行测试
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
输出结果
- 1
- 1
Callable和Future
为了让线程可以有返回值,从JDK1.5开始,Java提供Callable接口,Callable接口也提供了一个call()方法可以作为线程执行体,但call方法比run方法功能更强大
- call()方法可以有返回值
- call()方法可以抛出异常
因此我们完全可以提供一个Callable对象作为Thread的target对象,执行体就是call方法.问题是Callable不是Runnable接口的子接口,所以Callable不能直接作为Thread的target.
JDK1.5提供了Future接口来代表Callable接口里的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口–可以作为Thread类的target.
在Future里定义了如下几个公共方法来控制它关联的Callable任务
- boolean cancel(boolean mayInterruptIfRunning): 取消该Future里关联的Callable任务.
- V get(): 返回Callable任务里call的返回值,该方法会导致程序阻塞,必须等到子线程结束才会得到返回值.
- V get(long timeout, TimeUnit unit): 指定阻塞时间,如果超过时间还没返回值则抛出TimeoutException异常.
- boolean isCancelled(): 如果Callable任务正常执行完前被取消,返回true.
- boolean isDone(): 如果Callable任务已经完成,则返回true.
创建并启动有返回值的线程过程如下:
- 创建Callable接口的实现类,并实现call方法.注意Callable接口有泛型限制,Callable接口里的泛型参数必须和call的返回值相同.
- 创建Callable的实例,用FutureTask类来包装Callable对象,该FutureTask封装了call的返回值.
- 使用FutureTask对象作为Thread的target创建,启动新线程.
- 使用FutureTask对象的方法来获得子线程执行结束的返回值
下面通过一段程序来演示,先实现Callable接口
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
创建,启动
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
运行结果
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
成功得到了返回值,只是调用get()方法时会阻塞主线程
线程池
系统启动一个线程成本是比较高的,因为涉及与操作系统交互.这时应考虑使用线程池来提高性能.线程池在创建时就启动大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当该线程执行结束后并不会死亡,而是返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法.
使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时会导致JVM性能急剧下降,甚至导致崩溃,而线程池可以控制系统中并发线程的数量
JDK1.5以前都需要自己实现,以后的提供了一个Executors工厂类来产生线程池,而该工厂类包含如下几个静态工厂方法来创建线程池:
- newCachedThreadPool(): 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存到线程池中.
- newFixedThreadPool(int nThreads): 创建一个可重用,具有固定线程数的线程池.
- newSingleThreadExecutor(): 创建只有一条线程的线程池.
- ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池,它可以在指定延迟后执行任务.
- newSingleThreadScheduledExecutor(): 创建只有一条线程的线程池,它可以在指定延迟后执行任务.
上面的前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程.而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务.
ExecutorService代表尽快执行线程的线程池,ExecutorService里提供了如下三个方法:
- 1
- 2
- 3
- 1
- 2
- 3
ScheduledExecutorService代表可以在指定延迟或周期性执行线程任务的线程池,它提供了如下4个方法:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
当用完一个线程池后应该调用shutdown()方法,该方法将启动线程池的关闭序列,调用了shutdown()的线程池不再接受新的任务,但会将以前提交的任务执完.当线程池中所有的任务都执行完,线程池就会死亡.另外也可以调用shutdownNow()方法来关闭线程池,该方法将停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行任务的列表.
使用线程池的步骤如下:
- 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池.
- 创建Runnable或Callable的实例,作为线程执行任务.
- 调用ExecutorService对象的submit方法来提交Runnable或Callable的实例.
- 当不想提交任何任务时调用ExecutorService对象的shutdown()方法关闭线程池
下面进行一个简单的演示,更多复杂的用法请自己看api
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
运行结果
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
线程相关类
ThreadLocal类
ThreadLocal是Thread Local Variable(局部线程变量)的意思.局部线程变量的使用其实很简单,就是为每一个使用该变量的线程都提供一个该变量值的副本,每一个线程都可以独自的改变自己的副本,而不会和其它的线程副本冲突.从线程的角度来看,就好象每一个线程完全拥有了该变量.
ThreadLocal提供了如下三个方法:
- T get(): 返回此线程局部变量当前的副本值
- void remove() 删除此线程局部变量当前的副本值
- set(T value) 设置此线程局部变量当前的副本值
下面来演示ThreadLoacl的作用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
运行结果
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
从上面的null可以看出其实在account一进入新的Thread的时候就赋予了新的副本,因为我们的account本身是赋予了”Eric”这个名字的,但开始却读到了null,说明已经给予了新的副本
ThreadLocal只是将冲突的资源进行了多份复制,各自使用各自的,因此之间不能通信.ThreadLocal和锁机制解决的问题是有本质的区别的.
包装线程不安全的集合
Java中的ArrayList,LinkedList,HashSet,TreeSet,HashMap等都是线程不安全的,也就是有可能当多个线程向这个集合中放入一个元素时,可能会破坏这些数据集合的完整性.
如果程序有多条线程需要访问以上集合,我们可以使用Collections提供的静态方法把这些集合包装成线程安全的集合.Collections提供了如下几个静态方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
例如我们要使用多线程中线程安全的HashMap对象,可用如下方式:
- 1
- 1
线程安全的集合类
从JDK1.5开始,在java.util.concurrent包下提供了ConcurrentHashMap和ConcurrentLinkedDeque支持并发访问的集合,由于内部使用非常复杂的算法,支持并发访写,具体使用可以查看api