李涛,Sun中国工程研究院工程师
概述:
Java
自
1995
年面世以来得到了广泛得一个运用,但是对多线程编程的支持
Java
很长时间一直停留在初级阶段。在
Java 5.0
之前
Java
里的多线程编程主要是通过
Thread
类,
Runnable
接口,
Object
对象中的
wait()
、
notify()
、
notifyAll()
等方法和
synchronized
关键词来实现的。这些工具虽然能在大多数情况下解决对共享资源的管理和线程间的调度,但存在以下几个问题
1.
过于原始,拿来就能用的功能有限,即使是要实现简单的多线程功能也需要编写大量的代码。这些工具就像汇编语言一样难以学习和使用,比这更糟糕的是稍有不慎它们还可能被错误地使用,而且这样的错误很难被发现。
2.
如果使用不当,会使程序的运行效率大大降低。
3.
为了提高开发效率,简化编程,开发人员在做项目的时候往往需要写一些共享的工具来实现一些普遍适用的功能。但因为没有规范,相同的工具会被重复地开发,造成资源浪费。
4.
因为锁定的功能是通过
Synchronized
来实现的,这是一种块结构,只能对代码中的一段代码进行锁定,而且锁定是单一的。如以下代码所示:
synchronized
(
lock
)
{
//
执行对共享资源的操作
……
}
|
一些复杂的功能就很难被实现。比如说如果程序需要取得
lock A
和
lock B
来进行操作
1
,然后需要取得
lock C
并且释放
lock A
来进行操作
2
,
Java 5.0
之前的多线程框架就显得无能为力了。
因为这些问题,程序员对旧的框架一直颇有微词。这种情况一直到
Java 5.0
才有较大的改观,一系列的多线程工具包被纳入了标准库文件。这些工具包括了一个新的多线程程序的执行框架,使编程人员可方便地协调和调度线程的运行,并且新加入了一些高性能的常用的工具,使程序更容易编写,运行效率更高。本文将分类并结合例子来介绍这些新加的多线程工具。
在我们开始介绍
Java 5.0
里的新
Concurrent
工具前让我们先来看一下一个用旧的多线程工具编写的程序,这个程序里有一个
Server
线程,它需要启动两个
Component
,
Server
线程需等到
Component
线程完毕后再继续。相同的功能在
Synchronizer
一章里用新加的工具
CountDownLatch
有相同的实现。两个程序,孰优孰劣,哪个程序更容易编写,哪个程序更容易理解,相信大家看过之后不难得出结论。
public class ServerThread {
Object concLock = new Object();
int count = 2;
public void runTwoThreads() {
//
启动两个线程去初始化组件
new Thread(new ComponentThread1(this)).start();
new Thread(new ComponentThread1(this)).start();
// Wait for other thread
while(count != 0) {
synchronized(concLock) {
try {
concLock.wait();
System.out.println("Wake up.");
} catch (InterruptedException ie) { //
处理异常
}
}
}
System.out.println("Server is up.");
}
public void callBack() {
synchronized(concLock) {
count--;
concLock.notifyAll();
}
}
public static void main(String[] args){
ServerThread server = new ServerThread();
server.runTwoThreads();
}
}
public class ComponentThread1 implements Runnable {
private ServerThread server;
public ComponentThread1(ServerThread server) {
this.server = server;
}
public void run() {
//
做组件初始化的工作
System.out.println("Do component initialization.");
server.callBack();
}
}
|
1:三个新加的多线程包
Java 5.0
里新加入了三个多线程包:
java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks.
- java.util.concurrent包含了常用的多线程工具,是新的多线程工具的主体。
- java.util.concurrent.atomic包含了不用加锁情况下就能改变值的原子变量,比如说AtomicInteger提供了addAndGet()方法。Add和Get是两个不同的操作,为了保证别的线程不干扰,以往的做法是先锁定共享的变量,然后在锁定的范围内进行两步操作。但用AtomicInteger.addAndGet()就不用担心锁定的事了,其内部实现保证了这两步操作是在原子量级发生的,不会被别的线程干扰。
- java.util.concurrent.locks包包含锁定的工具。
2:Callable 和 Future接口
Callable
是类似于
Runnable
的接口,实现
Callable
接口的类和实现
Runnable
的类都是可被其它线程执行的任务。
Callable
和
Runnable
有几点不同:
- Callable规定的方法是call(),而Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call()方法可抛出异常,而run()方法是不能抛出异常的。
- 运行Callable任务可拿到一个Future对象,通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
以下是
Callable
的一个例子:
public class DoCallStuff implements Callable<String>{ // *1
private int aInt;
public DoCallStuff(int aInt) {
this.aInt = aInt;
}
public String call() throws Exception { //*2
boolean resultOk = false;
if(aInt == 0){
resultOk = true;
} else if(aInt == 1){
while(true){ //infinite loop
System.out.println("looping....");
Thread.sleep(3000);
}
} else {
throw new Exception("Callable terminated with Exception!"); //*3
}
if(resultOk){
return "Task done.";
} else {
return "Task failed";
}
}
}
|
*1:
名为
DoCallStuff
类实现了
Callable<String>
,
String
将是
call
方法的返回值类型。例子中用了
String
,但可以是任何
Java
类。
*2: call
方法的返回值类型为
String
,这是和类的定义相对应的。并且可以抛出异常。
*3: call
方法可以抛出异常,如加重的斜体字所示。
以下是调用
DoCallStuff
的主程序。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Executor {
public static void main(String[] args){
//*1
DoCallStuff call1 = new DoCallStuff(0);
DoCallStuff call2 = new DoCallStuff(1);
DoCallStuff call3 = new DoCallStuff(2);
//*2
ExecutorService es = Executors.newFixedThreadPool(3);
//*3
Future<String> future1 = es.submit(call1);
Future<String> future2 = es.submit(call2);
Future<String> future3 = es.submit(call3);
try {
//*4
System.out.println(future1.get());
//*5
Thread.sleep(3000);
System.out.println("Thread 2 terminated? :" + future2.cancel(true));
//*6
System.out.println(future3.get());
} catch (ExecutionException ex) {
ex.printStackTrace();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
|
*1:
定义了几个任务
*2:
初始了任务执行工具。任务的执行框架将会在后面解释。
*3:
执行任务,任务启动时返回了一个
Future
对象,如果想得到任务执行的结果或者是异常可对这个
Future
对象进行操作。
Future
所含的值必须跟
Callable
所含的值对映,比如说例子中
Future<String>
对印
Callable<String>
*4:
任务
1
正常执行完毕,
future1.get()
会返回线程的值
*5:
任务
2
在进行一个死循环,调用
future2.cancel(true)
来中止此线程。传入的参数标明是否可打断线程,
true
表明可以打断。
*6:
任务
3
抛出异常,调用
future3.get()
时会引起异常的抛出。
运行
Executor
会有以下运行结果:
looping....
Task done. //*1
looping....
looping....//*2
looping....
looping....
looping....
looping....
Thread 2 terminated? :true //*3
//*4
java.util.concurrent.ExecutionException: java.lang.Exception: Callable terminated with Exception!
at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:205)
at java.util.concurrent.FutureTask.get(FutureTask.java:80)
at concurrent.Executor.main(Executor.java:43)
…….
|
*1:
任务
1
正常结束
*2:
任务
2
是个死循环,这是它的打印结果
*3:
指示任务
2
被取消
*4:
在执行
future3.get()
时得到任务
3
抛出的异常
3:新的任务执行架构
在
Java 5.0
之前启动一个任务是通过调用
Thread
类的
start()
方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制同时执行的线程数量就需要额外编写代码来完成。
5.0
里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接池的线程池来执行任务。这个架构主要有三个接口和其相应的具体类组成。这三个接口是
Executor, ExecutorService
和
ScheduledExecutorService
,让我们先用一个图来显示它们的关系:
图的左侧是接口,图的右侧是这些接口的具体类。注意
Executor
是没有直接具体实现的。
Executor
接口:
是用来执行
Runnable
任务的,它只定义一个方法:
- execute(Runnable command):执行Ruannable类型的任务
ExecutorService
接口:
ExecutorService
继承了
Executor
的方法,并提供了执行
Callable
任务和中止任务执行的服务,其定义的方法主要有:
- submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
- invokeAll(collection of tasks):批处理任务集合,并返回一个代表这些任务的Future对象集合
- shutdown():在完成已提交的任务后关闭服务,不再接受新任务
- shutdownNow():停止所有正在执行的任务并关闭服务。
- isTerminated():测试是否所有任务都执行完毕了。
- isShutdown():测试是否该ExecutorService已被关闭
ScheduledExecutorService
接口
在
ExecutorService
的基础上,
ScheduledExecutorService
提供了按时间安排执行任务的功能,它提供的方法主要有:
- schedule(task, initDelay): 安排所提交的Callable或Runnable任务在initDelay指定的时间后执行。
- scheduleAtFixedRate():安排所提交的Runnable任务按指定的间隔重复执行
- scheduleWithFixedDelay():安排所提交的Runnable任务在每次执行完后,等待delay所指定的时间后重复执行。
代码:
ScheduleExecutorService
的例子
public class ScheduledExecutorServiceTest {
public static void main(String[] args)
throws InterruptedException, ExecutionException{
//*1
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
//*2
Runnable task1 = new Runnable() {
public void run() {
System.out.println("Task repeating.");
}
};
//*3
final ScheduledFuture future1 =
service.scheduleAtFixedRate(task1, 0, 1, TimeUnit.SECONDS);
//*4
ScheduledFuture<String> future2 = service.schedule(new Callable<String>(){
public String call(){
future1.cancel(true);
return "task cancelled!";
}
}, 5, TimeUnit.SECONDS);
System.out.println(future2.get());
//*5
service.shutdown();
}
}
|
这个例子有两个任务,第一个任务每隔一秒打印一句“
Task repeating
”
,
第二个任务在
5
秒钟后取消第一个任务。
*1:
初始化一个
ScheduledExecutorService
对象,这个对象的线程池大小为
2
。
*2:
用内函数的方式定义了一个
Runnable
任务。
*3:
调用所定义的
ScheduledExecutorService
对象来执行任务,任务每秒执行一次。能重复执行的任务一定是
Runnable
类型。注意我们可以用
TimeUnit
来制定时间单位,这也是
Java 5.0
里新的特征,
5.0
以前的记时单位是微秒,现在可精确到奈秒。
*4:
调用
ScheduledExecutorService
对象来执行第二个任务,第二个任务所作的就是在
5
秒钟后取消第一个任务。
*5:
关闭服务。
Executors
类
虽然以上提到的接口有其实现的具体类,但为了方便
Java 5.0
建议使用
Executors
的工具类来得到
Executor
接口的具体对象,需要注意的是
Executors
是一个类,不是
Executor
的复数形式。
Executors
提供了以下一些
static
的方法:
- callable(Runnable task): 将Runnable的任务转化成Callable的任务
- newSingleThreadExecutor: 产生一个ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
- newCachedThreadPool(): 产生一个ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要调整,线程执行完任务后返回线程池,供执行下一次任务使用。
- newFixedThreadPool(int poolSize):产生一个ExecutorService对象,这个对象带有一个大小为poolSize的线程池,若任务数量大于poolSize,任务会被放在一个queue里顺序执行。
- newSingleThreadScheduledExecutor:产生一个ScheduledExecutorService对象,这个对象的线程池大小为1,若任务多于一个,任务将按先后顺序执行。
- newScheduledThreadPool(int poolSize): 产生一个ScheduledExecutorService对象,这个对象的线程池大小为poolSize,若任务数量大于poolSize,任务会在一个queue里等待执行
以下是得到和使用
ExecutorService
的例子:
代码:如何调用
Executors
来获得各种服务对象
//Single Threaded ExecutorService
ExecutorService singleThreadeService = Executors.newSingleThreadExecutor();
//Cached ExecutorService
ExecutorService cachedService = Executors.newCachedThreadPool();
//Fixed number of ExecutorService
ExecutorService fixedService = Executors.newFixedThreadPool(3);
//Single ScheduledExecutorService
ScheduledExecutorService singleScheduledService =
Executors.newSingleThreadScheduledExecutor();
//Fixed number of ScheduledExecutorService
ScheduledExecutorService fixedScheduledService =
Executors.newScheduledThreadPool(3);
|
4:Lockers和Condition接口
在多线程编程里面一个重要的概念是锁定,如果一个资源是多个线程共享的,为了保证数据的完整性,在进行事务性操作时需要将共享资源锁定,这样可以保证在做事务性操作时只有一个线程能对资源进行操作,从而保证数据的完整性。在
5.0
以前,锁定的功能是由
Synchronized
关键字来实现的,这样做存在几个问题:
- 每次只能对一个对象进行锁定。若需要锁定多个对象,编程就比较麻烦,一不小心就会出现死锁现象。
- 如果线程因拿不到锁定而进入等待状况,是没有办法将其打断的
在
Java 5.0
里出现两种锁的工具可供使用,下图是这两个工具的接口及其实现:
Lock
接口
ReentrantLock
是
Lock
的具体类,
Lock
提供了以下一些方法:
- lock(): 请求锁定,如果锁已被别的线程锁定,调用此方法的线程被阻断进入等待状态。
- tryLock():如果锁没被别的线程锁定,进入锁定状态,并返回true。若锁已被锁定,返回false,不进入等待状态。此方法还可带时间参数,如果锁在方法执行时已被锁定,线程将继续等待规定的时间,若还不行才返回false。
- unlock():取消锁定,需要注意的是Lock不会自动取消,编程时必须手动解锁。
代码:
//
生成一个锁
Lock lock = new ReentrantLock();
public void accessProtectedResource() {
lock.lock(); //
取得锁定
try {
//
对共享资源进行操作
} finally {
//
一定记着把锁取消掉,锁本身是不会自动解锁的
lock.unlock();
}
}
|
ReadWriteLock
接口
为了提高效率有些共享资源允许同时进行多个读的操作,但只允许一个写的操作,比如一个文件,只要其内容不变可以让多个线程同时读,不必做排他的锁定,排他的锁定只有在写的时候需要,以保证别的线程不会看到数据不完整的文件。
ReadWriteLock
可满足这种需要。
ReadWriteLock
内置两个
Lock
,一个是读的
Lock
,一个是写的
Lock
。多个线程可同时得到读的
Lock
,但只有一个线程能得到写的
Lock
,而且写的
Lock
被锁定后,任何线程都不能得到
Lock
。
ReadWriteLock
提供的方法有:
- readLock(): 返回一个读的lock
- writeLock(): 返回一个写的lock, 此lock是排他的。
ReadWriteLock
的例子:
public class FileOperator{
//
初始化一个
ReadWriteLock
ReadWriteLock lock = new ReentrantReadWriteLock();
public String read() {
//
得到
readLock
并锁定
Lock readLock = lock.readLock();
readLock.lock();
try {
//
做读的工作
|