讲到Java多线程,大多数人脑海中跳出来的是Thread、Runnable、synchronized……这些是最基本的东西,虽然已经足够强大,但想要用好还真不容易。从JDK1.5开始,增加了java.util.concurrent包,它的引入大大简化了多线程程序的开发(要感谢一下大牛Doug Lee)。
java.util.concurrent包分成了三个部分,分别是java.util.concurrent、java.util.concurrent.atomic和java.util.concurrent.lock。内容涵盖了并发集合类、线程池机制、同步互斥机制、线程安全的变量更新工具类、锁等等常用工具。
为了便于理解,本文使用一个例子来做说明,交代一下它的场景:
假设要对一套10个节点组成的环境进行检查,这个环境有两个入口点,通过节点间的依赖关系可以遍历到整个环境。依赖关系可以构成一张有向图,可能存在环。为了提高检查的效率,考虑使用多线程。
1、Executors
通过这个类能够获得多种线程池的实例,例如可以调用newSingleThreadExecutor()获得单线程的ExecutorService,调用newFixedThreadPool()获得固定大小线程池的ExecutorService。拿到ExecutorService可以做的事情就比较多了,最简单的是用它来执行Runnable对象,也可以执行一些实现了Callable<T>的对象。用Thread的start()方法没有返回值,如果该线程执行的方法有返回值那用ExecutorService就再好不过了,可以选择submit()、invokeAll()或者invokeAny(),根据具体情况选择合适的方法即可。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* 线程池服务类
*
* @author DigitalSonic
*/
public class ThreadPoolService {
/**
* 默认线程池大小
*/
public static final int DEFAULT_POOL_SIZE = 5;
/**
* 默认一个任务的超时时间,单位为毫秒
*/
public static final long DEFAULT_TASK_TIMEOUT = 1000;
private int poolSize = DEFAULT_POOL_SIZE;
private ExecutorService executorService;
/**
* 根据给定大小创建线程池
*/
public ThreadPoolService(int poolSize) {
setPoolSize(poolSize);
}
/**
* 使用线程池中的线程来执行任务
*/
public void execute(Runnable task) {
executorService.execute(task);
}
/**
* 在线程池中执行所有给定的任务并取回运行结果,使用默认超时时间
*
* @see #invokeAll(List, long)
*/
public List<Node> invokeAll(List<ValidationTask> tasks) {
return invokeAll(tasks, DEFAULT_TASK_TIMEOUT * tasks.size());
}
/**
* 在线程池中执行所有给定的任务并取回运行结果
*
* @param timeout 以毫秒为单位的超时时间,小于0表示不设定超时
* @see java.util.concurrent.ExecutorService#invokeAll(java.util.Collection)
*/
public List<Node> invokeAll(List<ValidationTask> tasks, long timeout) {
List<Node> nodes = new ArrayList<Node>(tasks.size());
try {
List<Future<Node>> futures = null;
if (timeout < 0) {
futures = executorService.invokeAll(tasks);
} else {
futures = executorService.invokeAll(tasks, timeout, TimeUnit.MILLISECONDS);
}
for (Future<Node> future : futures) {
try {
nodes.add(future.get());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return nodes;
}
/**
* 关闭当前ExecutorService
*
* @param timeout 以毫秒为单位的超时时间
*/
public void destoryExecutorService(long timeout) {
if (executorService != null && !executorService.isShutdown()) {
try {
executorService.awaitTermination(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
/**
* 关闭当前ExecutorService,随后根据poolSize创建新的ExecutorService
*/
public void createExecutorService() {
destoryExecutorService(1000);
executorService = Executors.newFixedThreadPool(poolSize);
}
/**
* 调整线程池大小
* @see #createExecutorService()
*/
public void setPoolSize(int poolSize) {
this.poolSize = poolSize;
createExecutorService();
}
}
invokeAll()和invokeAny()方法。前者会执行给定的所有Callable<T>对象,等所有任务完成后返回一个包含了执行结果的List<Future<T>>,每个Future.isDone()都是true,可以用Future.get()拿到结果;后者只要完成了列表中的任意一个任务就立刻返回,返回值就是执行结果。
2、Lock
多线程编程中常常要锁定某个对象,之前会用synchronized来实现,现在又多了另一种选择,那就是java.util.concurrent.locks。通过Lock能够实现更灵活的锁定机制,它还提供了很多synchronized所没有的功能,例如尝试获得锁(tryLock())。
使用Lock时需要自己获得锁并在使用后手动释放,这一点与synchronized有所不同,所以通常Lock的使用方式是这样的
Lock l = ...;
l.lock();
try {
// 执行操作
} finally {
l.unlock();
}
java.util.concurrent.locks中提供了几个Lock接口的实现类,比较常用的应该是ReentrantLock。
讲到Lock,就不能不讲Conditon,前者代替了synchronized,而后者则代替了Object对象上的wait()、notify()和notifyAll()方法(Condition中提供了await()、signal()和signalAll()方法),当满足运行条件前挂起线程。Condition是与Lock结合使用的,通过Lock.newCondition()方法能够创建与Lock绑定的Condition实例。JDK的JavaDoc中有一个例子能够很好地说明Condition的用途及用法:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
3、并发集合类
java.util包中的集合类有的是线程安全的,有的则不是,在编写多线程的程序时使用线程安全的类能省去很多麻烦,但这些类的性能如何呢?java.util.concurrent包中提供了几个并发结合类,例如ConcurrentHashMap、ConcurrentLinkedQueue和CopyOnWriteArrayList等等,根据不同的使用场景,开发者可以用它们替换java.util包中的相应集合类。
CopyOnWriteArrayList是ArrayList的一个变体,比较适合用在读取比较频繁、修改较少的情况下,因为每次修改都要复制整个底层数组。ConcurrentHashMap中为Map接口增加了一些方法(例如putIfAbsenct()),同时做了些优化,下面的代码中使用ConcurrentHashMap来作为全局节点表,完全无需考虑并发问题。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 执行验证的服务类
*
* @author DigitalSonic
*/
public class ValidationService {
/**
* 全局节点表
*/
public static final Map<String, Node> NODE_MAP = new ConcurrentHashMap<String, Node>();
private ThreadPoolService threadPoolService;
public ValidationService(ThreadPoolService threadPoolService) {
this.threadPoolService = threadPoolService;
}
/**
* 给出一个入口节点的WSDL,通过广度遍历的方式验证与其相关的各个节点
*
* @param wsdl 入口节点WSDL
*/
public void validate(List<String> wsdl) {
List<String> visitedNodes = new ArrayList<String>();
List<String> nextRoundNodes = new ArrayList<String>();
nextRoundNodes.addAll(wsdl);
while (nextRoundNodes.size() > 0) {
List<ValidationTask> tasks = getTasks(nextRoundNodes);
List<Node> nodes = threadPoolService.invokeAll(tasks);
visitedNodes.addAll(nextRoundNodes);
nextRoundNodes.clear();
getNextRoundNodes(nodes, visitedNodes, nextRoundNodes);
}
}
private List<String> getNextRoundNodes(List<Node> nodes,
List<String> visitedNodes, List<String> nextRoundNodes) {
for (Node node : nodes) {
for (String wsdl : node.getDependencies()) {
if (!visitedNodes.contains(wsdl)) {
nextRoundNodes.add(wsdl);
}
}
}
return nextRoundNodes;
}
private List<ValidationTask> getTasks(List<String> nodes) {
List<ValidationTask> tasks = new ArrayList<ValidationTask>(nodes.size());
for (String wsdl : nodes) {
tasks.add(new ValidationTask(wsdl));
}
return tasks;
}
}
4、AtomicInteger
对变量的读写操作都是原子操作(除了long或者double的变量),但像数值类型的++--操作不是原子操作,像i++中包含了获得i的原始值、加1、写回i、返回原始值,在进行类似i++这样的操作时如果不进行同步问题就大了。好在java.util.concurrent.atomic为我们提供了很多工具类,可以以原子方式更新变量。
以AtomicInteger为例,提供了代替++--的getAndIncrement()、incrementAndGet()、getAndDecrement()和decrementAndGet()方法,还有加减给定值的方法、当前值等于预期值时更新的compareAndSet()方法。