线程组和未处理的异常
Java使用ThreadGroup来代表线程组
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组
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):创建新线程,新线程名为name,属于group线程组。
创建线程组
Thread类没有提供setThreadGroup的方法来改变线程所属的线程组,但提供了一个getThreadGroup()方法来返回该线程所属的线程组。
getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。ThreadGroup类有如下两个简单的构造器来创建实例:
ThreadGroup(String name):以指定线程组名字来创建新的线程组。
ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组。怎样把线程放入指定的线程组中呢?
–在创建一个Thread实例时,通过传入的ThreadGroup对象,即可将该线程放入指定的线程组。通过线程组对这批线程进行整体的管理。
ThreadGroup提供了如下两个方法:
- setDaemon(boolean daemon):控制将线程组本身设置为后台线程组。并不是将它包含的线程设置为后台线层。 如果它包含的所有线程都死了,后台线程组本身也会自动销毁。
- setMaxPriority(优先级):它是设置线程租的最高优先级。 该线程组已有的线程的优先级不会受影响。对以后新添加的线程的优先级才会有影响。
在JDK1.5以前:如果线程出现了异常,系统会自动回调他所在的线程组的uncaughtException(Thread t,Throwable e)方法来修复该异常。
在JDK 1.5之后,线程允许自行设置“异常处理器”,无需线程组。
Thread.setDefaultUncaughtExceptionHandler:为所有线程(包括主线程)设置磨人的异常处理器。
实例.setUncaughtExceptionHandler:为当前线程实例设置异常处理器。
【实例设置的异常处理器会覆盖默认的异常处理器】线程组处理异常的默认流程
- 如果该线程组有父线程组,则调用父线程组的uncaughtException方法来处理该异常。
- 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器),那就调用该异常处理器来处理该异常。
- 如果该异常对象是ThreadDeath的对象,将不做任何处理;否则将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。
程序实例
class MyThread extends Thread
{
// 提供指定线程名的构造器
public MyThread(String name)
{
super(name);
}
// 提供指定线程名、线程组的构造器
public MyThread(ThreadGroup group , String name)
{
super(group, name);
}
public void run()
{
for (int i = 0; i < 20 ; i++ )
{
System.out.println(getName() + " 线程的i变量" + i);
}
}
}
public class ThreadGroupTest
{
public static void main(String[] args)
{
// 获取主线程所在的线程组,这是所有线程默认的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名字:"
+ mainGroup.getName());
System.out.println("主线程组是否是后台线程组:"
+ mainGroup.isDaemon());
new MyThread("主线程组的线程").start();
ThreadGroup tg = new ThreadGroup("新线程组");
tg.setDaemon(true);
System.out.println("tg线程组是否是后台线程组:"
+ tg.isDaemon());
MyThread tt = new MyThread(tg , "tg组的线程甲");
tt.start();
new MyThread(tg , "tg组的线程乙").start();
}
}
线程池
池(Pool)的本质,就是一种“缓存”技术。
是否要缓存一个对象,取决于该对象的创建成本。当然也要考虑到系统内存大小。
缓存的本质:牺牲内存空间,来换取时间成本。
资源池(Resource Pool): 用于解决资源的频繁请求、释放所造成的性能下降。
线程对象的创建成本,比较大。虽然创建线程的成本,比创建进程的成本要小得多。但相比普通Java对象,Thread的创建成本依然较大。
为了解决这个问题,我们使用线程池。
系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
JDK1.5提供了一个Executors工厂类来产生线程池,该工厂类里包含如下几个==静态工厂方法==来创建连接池:
* newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
* newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
* newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为1。
* newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
* newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。
ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象(代表线程任务)提交给该线程池即可,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法:
1. Future
使用线程池的步骤
(1)调用Executors类的静态工厂方法创建一个ExecutorService对象或ScheduledExecutorService对象,其中前者代表简单的线程池,后者代表能以任务调度方式执行线程的线程池。
(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
(3)调用ExecutorService对象的submit方法来提交Runnable实例或Callable实例;或调用ScheduledExecutorService的schedule来执行线程。
(4)当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池。
==使用Lambda表达式直接创建对象,可以无需先创建实现类,再创建对象,详见P722==
程序实例
public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
//创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//使用Lambda表达式创建Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "的i的值为: " + i);
}
};
//向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
//关闭线程池
pool.shutdown();
}
}
Java8增强的ForkJoinPool
Java提供了ForkJoinPool来支持充分利用多CPU、多核CPU的优势,可以把一个任务拆分成多个小任务,把多个小任物放到多个处理器核心上并行执行;当多个小任物执行完之后,再将这些执行结果合并起来。
详见P754
程序实例
1 没有返回值的任务 RecursiveAction
//继承RecursiveAction来实现“可分解”不需要返回值的任务
class PrintTask extends RecursiveAction {
//每个小任务最多打印50个数字
private static final int THRESHOLD = 50;
private int start;
private int end;
//打印从start到end的任务
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
//当end与static之间的差小于THRESHOLD时,开始打印
if (end - start < THRESHOLD) {
for(int i=start;i<end;i++){
System.out.println(Thread.currentThread().getName() + "的i值" + i);
}
}
else
{
//当end与start之间的差大于50,将任务分解
int middle = (start+end)/2;
PrintTask left = new PrintTask(start,middle);
PrintTask right= new PrintTask(middle,end);
//并行执行两个小任务
left.fork();
right.fork();
}
}
}
public class ForkJoinPoolTest {
public static void main(String[] args)throws Exception{
ForkJoinPool pool = new ForkJoinPool();
//提交可分解的PrintTask任务
pool.submit(new PrintTask(0,300));
pool.awaitTermination(2, TimeUnit.SECONDS);
//关闭线程池
pool.shutdown();
}
}
2 有返回值的任务: RecursiveTask , 泛型参数T代表了该任务的返回值
import java.util.concurrent.*;
import java.util.*;
// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer>
{
// 每个“小任务”只最多只累加20个数
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加从start到end的数组元素
public CalTask(int[] arr , int start, int end)
{
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute()
{
int sum = 0;
// 当end与start之间的差小于THRESHOLD时,开始进行实际累加
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
sum += arr[i];
}
return sum;
}
else
{
// 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
// 将大任务分解成两个小任务。
int middle = (start + end) / 2;
CalTask left = new CalTask(arr , start, middle);
CalTask right = new CalTask(arr , middle, end);
// 并行执行两个“小任务”
left.fork();
right.fork();
// 把两个“小任务”累加的结果合并起来
return left.join() + right.join(); // ①
}
}
}
public class Sum
{
public static void main(String[] args)
throws Exception
{
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
// 初始化100个数字元素
for (int i = 0 , len = arr.length; i < len ; i++ )
{
int tmp = rand.nextInt(20);
System.out.println(tmp);
// 对数组元素赋值,并将数组元素的值添加到sum总和中。
total += (arr[i] = tmp);
}
System.out.println(total);
// 创建一个通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交可分解的CalTask任务
Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
System.out.println(future.get());
// 关闭线程池
pool.shutdown();
}
}
==其中Random类专门用来生成随机数, nextInt(20)方法就是代表生成0~20的随机数(包含0,但不包括20)。==
# 线程相关类
## ThreadLocal类
ThreadLocal,是Thread Local Variable(线程局部变量)的意思,也许将它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
ThreadLocal类的用法非常简单,它只提供了如下三个public方法:
1. T get():返回此线程局部变量中当前线程副本中的值。
2. void remove():删除此线程局部变量中当前线程的值。
3. void set(T value):设置此线程局部变量中当前线程副本中的值。
包装线程不安全的集合
如果程序有多条线程可能访问以上ArrayList、HashMap等集合,可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法:
- static Collection synchronizedCollection(Collection c):返回指定 collection 对应的线程安全的collection。
- static List synchronizedList(List list):返回指定List对应的线程安全的List对象。
- static
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap m =Collections.synchronizedMap(new HashMap());
如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装。
线程安全的集合类
java.util.concurrent包下提供了提供了ConcurrentHashMap、ConcurrentLinkedQueue两个支持并发访问的集合,它们分别代表了支持并发访问的HashMap和支持并发访问的Queue。
当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。
详见P760