线程组和未处理的异常
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): 设置线程组的最高优先级
接下来就其中一些方法进行使用演示
public class TestThread extends Thread{
public TestThread(String name){
super(name);
}
public TestThread(ThreadGroup group,String name){
super(group,name);
}
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(getName()+"线程i的变量"+i);
}
}
}
测试
public class Test {
public static void main(String []args){
ThreadGroup mainGroup=Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名字:"+mainGroup.getName());
System.out.println("主线程组是否是后台线程:"+mainGroup.isDaemon());
new TestThread("主线程组的线程").start();
ThreadGroup threadGroup=new ThreadGroup("新线程组");
threadGroup.setDaemon(true);
System.out.println("threadGroup线程组是否是后台线程组:"+threadGroup.isDaemon());
TestThread A=new TestThread(threadGroup,"threadGroup线程组A");
A.start();
TestThread B=new TestThread(threadGroup,"threadGroup线程组B");
B.start();
}
}
运行结果:
主线程组的名字:main
主线程组是否是后台线程:false
threadGroup线程组是否是后台线程组:true
主线程组的线程线程i的变量0
主线程组的线程线程i的变量1
threadGroup线程组A线程i的变量0
threadGroup线程组A线程i的变量1
threadGroup线程组B线程i的变量0
threadGroup线程组B线程i的变量1
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的
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);
}
}
}
- 如果该线程组有父线程,则调用父线程组的uncaughtException方法来处理该异常.
- 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器,此异常处理器是为该线程所有实例设置的),那就调用该处理器来处理器处理该异常.
- 如果该异常对象是ThreadDeath对象,将不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程.
接下来我们自己实现Thread.UncaughtExceptionHandler进行演示
public class MyExHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t+"线程出现了异常:"+e);
}
}
在主线程进行测试
public class TestHandler {
public static void main(String []args){
Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
int a=5/0;//除数为0导致出现错误
}
}
输出结果
Thread[main,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero
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接口
public class RtnThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "的循环变量i=" +i);
}
return i;
}
}
创建,启动
public class CallableTest {
public static void main(String []args){
RtnThread rtnThread=new RtnThread();
FutureTask<Integer> task=new FutureTask<Integer>(rtnThread);
new Thread(task,"有返回值的线程").start();
try{
System.out.println("子线程的返回值:"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
运行结果
有返回值的线程的循环变量i=17
有返回值的线程的循环变量i=18
有返回值的线程的循环变量i=19
子线程的返回值:20
成功得到了返回值,只是调用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. Future<?> submit(Runnable task): 将一个Runnable对象交给指定的线程池.线程池将在有空闲的时执行Runnable对象所代表的任务.其中Future对象代表Runnable任务的返回值--但run没有返回值,所以Future对象在run执行结束后返回null.但可以调用Future的isDone(),isCancelled()方法来判断Runnable对象的执行状态.
2. <T> Future<T> submit(Runnable task, T result): 将一个Runnable对象交给指定的线程池.线程池将在有空闲的时执行Runnable对象所代表的任务.result显示指定线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result.
3. <T> Future<T> submit(Callable<T> task): 将一个Callable对象交给指定的线程池.线程池将在有空闲的时执行Callable对象所代表的任务.Future代表Callable对象里的call的返回值.
ScheduledExecutorService代表可以在指定延迟或周期性执行线程任务的线程池,它提供了如下4个方法:
1. ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit): 指定任务在延迟delay后执行
2. <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit): 指定任务在延迟delay后执行
3. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit): 在延迟之后按照一定的周期执行
4. ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit): initialDelay表示启动之前的延迟,delay表示启动以后,在每一次执行终止和下一次执行开始之间都给定delay延迟.如果任务的一次执行遇到异常就会取消后续执行
当用完一个线程池后应该调用shutdown()方法,该方法将启动线程池的关闭序列,调用了shutdown()的线程池不再接受新的任务,但会将以前提交的任务执完.当线程池中所有的任务都执行完,线程池就会死亡.另外也可以调用shutdownNow()方法来关闭线程池,该方法将停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行任务的列表.
使用线程池的步骤如下:
- 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池.
- 创建Runnable或Callable的实例,作为线程执行任务.
- 调用ExecutorService对象的submit方法来提交Runnable或Callable的实例.
- 当不想提交任何任务时调用ExecutorService对象的shutdown()方法关闭线程池
下面进行一个简单的演示,更多复杂的用法请自己看api
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的i值为"+i);
}
}
}
public class ThreadPoolTest {
public static void main(String []args){
ExecutorService pool= Executors.newFixedThreadPool(7);
pool.submit(new TestThread());
pool.submit(new TestThread());
pool.shutdown();
}
}
运行结果
pool-1-thread-1的i值为98
pool-1-thread-1的i值为99
pool-1-thread-2的i值为0
pool-1-thread-2的i值为1
pool-1-thread-2的i值为2
线程相关类
ThreadLocal类
ThreadLocal是Thread Local Variable(局部线程变量)的意思.局部线程变量的使用其实很简单,就是为每一个使用该变量的线程都提供一个该变量值的副本,每一个线程都可以独自的改变自己的副本,而不会和其它的线程副本冲突.从线程的角度来看,就好象每一个线程完全拥有了该变量.
ThreadLocal提供了如下三个方法:
- T get(): 返回此线程局部变量当前的副本值
- void remove() 删除此线程局部变量当前的副本值
- set(T value) 设置此线程局部变量当前的副本值
下面来演示ThreadLoacl的作用
public class Account {
private ThreadLocal<String> name = new ThreadLocal<String>();
public Account(String name) {
this.name.set(name);
}
public String getName() {
return this.name.get();
}
public void setName(String name) {
this.name.set(name);
}
}
public class MyTest extends Thread {
private Account account;
private String name;
public MyTest(Account account, String name) {
this.account = account;
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(account.getName() + "的i=" + i);
}
account.setName(name);
for (int i = 0; i < 2; i++) {
System.out.println(account.getName() + "的i=" + i);
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
Account account = new Account("Eric");
new MyTest(account, "A").start();
new MyTest(account, "B").start();
}
}
运行结果
null的i=0
null的i=0
null的i=1
null的i=1
A的i=0
B的i=0
A的i=1
B的i=1
从上面的null可以看出其实在account一进入新的Thread的时候就赋予了新的副本,因为我们的account本身是赋予了”Eric”这个名字的,但开始却读到了null,说明已经给予了新的副本
ThreadLocal只是将冲突的资源进行了多份复制,各自使用各自的,因此之间不能通信.ThreadLocal和锁机制解决的问题是有本质的区别的.
包装线程不安全的集合
Java中的ArrayList,LinkedList,HashSet,TreeSet,HashMap等都是线程不安全的,也就是有可能当多个线程向这个集合中放入一个元素时,可能会破坏这些数据集合的完整性.
如果程序有多条线程需要访问以上集合,我们可以使用Collections提供的静态方法把这些集合包装成线程安全的集合.Collections提供了如下几个静态方法:
1. static <T> Collection<T> synchronizedCollection(Collection<T> c): 返回指定collection对应的线程安全的collection.
2. static <T> List<T> synchronizedList(List<T> list): 返回指定List对应的线程安全的List对象.
3. static <K,V> Map<K,V> synchronizedMap(Map<K,V> m): 返回指定Map对应的线程安全的Map对象.
4. static <T> Set<T> synchronizedSet(Set<T> s): 返回指定Set对应的线程安全的Set对象.
5. static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m): 返回指定SortedMap对应的线程安全的SortedMap对象.
6. static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s): 返回指定SortedSet对应的线程安全的SortedSet对象.
例如我们要使用多线程中线程安全的HashMap对象,可用如下方式:
Map m=Collections.synchronizedMap(new HashMap<>());
线程安全的集合类
从JDK1.5开始,在java.util.concurrent包下提供了ConcurrentHashMap和ConcurrentLinkedDeque支持并发访问的集合,由于内部使用非常复杂的算法,支持并发访写,具体使用可以查看api