在多线程基础篇中讲述了线程的基础知识,本文将从两个方面继续讨论多线程的使用:
1.线程池的使用
2.ThreadLocal在多线程编程中的应用
线程池
线程池将任务的提交和执行进行解耦。调用者只需要将任务提交至线程池,由线程池负责执行并返回执行后的结果。下面我们来看一个线程池使用的例子。在这个例子中线程池就像一个boss,它雇佣了2个线程负责替它执行任务。
public class ThreadPool {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
ExecutorService boss = Executors.newFixedThreadPool(2);
for (int i = 0; i <= 2000; i++) {
boss.execute(new Runnable() {
public void run() {
System.out.println("hello world");
}
});
}
}
}
虽然线程池的使用相对比较简单,接下来我们来分析下线程池是怎么处理我们提交的任务的。在介绍之前我们来看下线程池的类图结构。Executor、ExecutorService接口定义了线程池中使用的方法。ThreadPoolExecutor实现了具体的线程池方法,ScheduledThreadPoolExecutor定义了定时任务线程池中的方法。
在上例中我们仅调用了Executors.newFixedThreadPool(2)创建了固定线程个数的线程池。实际上在调用该方法时,线程池工厂方法创建了一个ThreadPoolExecutor类的实例,并为我们指定了创建线程池时所采用的参数。接下来我们进一步分析在创建线程池时我们可以设置什么参数,这些参数的含义又是什么,以及线程池执行任务的流程是什么?
1.ThreadPoolExecutor类的构造方法
ThreadPoolExecutor类提供了4种构造方法,其中有3种方法都调用了第4种方法进行初始化线程池。
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize:线程池的基本大小,即核心线程的个数。
maximumPoolSize:线程池允许创建线程个数的最大值。
keepAliveTime:线程保持活动时间。当线程池的数量超过核心线程数时,剩余空闲的线程能够存活的时间。
unit:线程保持活动的时间单位。枚举的时间值有:DAYS、HOURS、MINUTES、MILLISECONDS、MICROSECONDS。
workQueue:任务队列。线程池会将处理不过来的任务插入该任务队列中。该任务队里是一个线程安全的队列。
threadFactory:创建线程的工厂。线程池中的线程都由该工厂创建。在创建线程时我们可以指定线程名称等属性。
RejectedExecutionHandler:拒绝策略。当线程池的线程数超过最大线程个数且线程池的任务队列已经满时,线程池会采用该策略进行处理新来的任务。具体的拒绝策略包括有:
a)AbortPolicy:直接抛异常(默认策略)
b)CallerRunsPolicy:在调用者的线程中直接调用任务的run方法
c)DiscardOlestPolicy:丢弃任务队列头部的任务,并执行当前任务
d)DiscardPolicy:不处理,直接丢弃任务
2.线程池的运行过程
下图是线程池在执行任务时的流程图。在流程图中我们可以看到线程池在执行任务时线程个数的变化,以及任务队列的变化。
那么线程池中的线程是如何执行任务以及消费任务队列中的任务的。在ThreadPoolExecutor类中保存了一个线程池中的线程集合。如下所示:
private final HashSet<Worker> workers = new HashSet<Worker>();
在Worker类中定义了线程的处理过程,代码如下:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
//线程执行任务的过程:先把刚分配的任务执行掉,然后在去处理任务队列中的任务
while (task != null || (task = getTask()) != null) {
runTask(task);//执行任务
task = null;//标记任务被执行完
}
} finally {
//当分配给该线程的任务或者任务队列中的任务都被消费完就从线程集合中删除该线程
workerDone(this);
}
}
}
上面我们介绍了线程池的创建以及线程池处理任务的流程,最后我们看下如何关闭线程池。在关闭线程池时我们可以采用shutdown和shutdownNow这两种方式进行关闭。其中shutdown方法将线程池的状态设置成SHUTDOWN状态,然后中断所有没有在执行任务的线程;shutdownNow方法先将线程池的状态设置成STOP,然后遍历线程池中的线程,并逐一调用线程的interrupt方法中断线程。
ThreadLocal
ThreadLocal可以用于保存线程内共享的,线程间互斥的值。具体的说ThreadLocal里面保存的变量是线程独占的,其他线程不能够对该变量进行修改。使用ThreadLocal的好处是在线程内部我们可以使用ThreadLocal传递参数。在使用ThreadLocal时我们可能存在着几个误区。
误区1:ThreadLocal可以解决共享对象多线程访问问题
实际上ThreadLocal不能解决该问题,如果调用ThreadLocal.set保存进去的对象是一个多线程共享的变量,那么多个线程调用ThreadLocal.get获取这个变量仍是这个共享变量,因而还存在着并发的问题。
误区2:ThreadLocal中存在着一个全局的Map成员变量
其实在每个Thread对象中都存在着一个ThreadLocalMap属性,在这个map中以ThreadLocal对象作为key,value存放者是线程内共享的变量。
最后我们来看下ThreadLocal中的get与set方法的实现。
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取map中以ThreadLocal对象为Key的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
//如果map中的值为空则调用该返回放回value,在创建ThreadLocal时我们可以实现其中的initialValue()方法指定初始值
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果线程中该属性为空,则创建ThreadLocalMap,否则设置以该ThreadLocal对象为key的值
if (map != null) {
map.set(this,value);
} else {
createMap(t, value);
}
}