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 包含锁定的工具。
1.Callable 和 Future 接口
新引入的 Callable 是类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 的类都是可被其它线程执行的任务。 Callable 和 Runnable 有几点不同:
Callable 规定的方法是 call() ,而 Runnable 规定的方法是 run().
Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。
call() 方法可抛出异常,而 run() 方法是不能抛出异常的。
运行 Callable 任务可拿到一个 Future 对象,通过 Future 对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
下面是一个简单的 Callable 例子。它实现了 Callable<String> , String 将是 call 方法的返回值类型,并且可以抛出异常
public class CallSample implements Callable<String> {
/*
* (non-Javadoc)
*
* @see java.util.concurrent.Callable#call()
*/
private int aInt;
public CallSample(int aInt) {
this.aInt = aInt;
}
public String call() throws Exception {
boolean resultOk = false;
if (aInt == 0) {
resultOk = true;
} else if (aInt == 1) {
while (true) {
System.out.println("looping....");
Thread.sleep(3000);
}
} else {
throw new Exception("Callable terminated with Exception!");
}
return resultOk?"Task done":"Task failed";
}
定义一个测试程序来运行下。建立一个拥有固定 3 个线程的线程池,分别执行正常结束、无限循环、抛出异常这 3 种情况的 Callable
public class Executor {
public static void main(String[] args) {
// 分别给定 3 种情况的 Callable
CallSample call1 = new CallSample(0);
CallSample call2 = new CallSample(1);
CallSample call3 = new CallSample(2);
ExecutorService es = Executors.newFixedThreadPool(3);
Future<String> future1 = es.submit(call1);
Future<String> future2 = es.submit(call2);
Future<String> future3 = es.submit(call3);
try {
System.out.println(future1.get());
Thread.sleep(3000);
System.out.println("Thread 2 terminated? :" + future2.cancel(true));
System.out.println(future3.get());
} catch (ExecutionException ex) {
ex.printStackTrace();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
2. 新的线程执行方式
在 Java 5.0 之前启动一个任务是通过调用 Thread 类的 start() 方法来实现的,任务的提交和执行是同时进行的,如果你想对任务的执行进行调度或是控制同时执行的线程数量就需要额外编写代码来完成。 5.0 里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接池的线程池来执行任务。这个架构主要有三个接口和其相应的具体类组成。这三个接口是 Executor, ExecutorService 和 ScheduledExecutorService 。
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 所指定的时间后重复执行 .
public class ScheduledExecutorServiceTest {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
// 初始化一个 ScheduledExecutorService 对象,这个对象的线程池大小为 2
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
// 定义了一个 Runnable 任务。
Runnable task1 = new Runnable() {
public void run() {
System.out.println("Task repeating.");
}
};
/*
* 调用所定义的 ScheduledExecutorService 对象来执行任务,任务每秒执行一次。能重复执行的任务一定是 Runnable 类型。我们可以用 TimeUnit 来制定时间单位。
*/
final ScheduledFuture future1 = service.scheduleAtFixedRate(task1, 0,
1, TimeUnit.SECONDS);
// 调用 ScheduledExecutorService 对象来执行第二个任务,第二个任务所作的就是在 5 秒钟后取消第一个任务
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());
service.shutdown();
}
3.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 里等待执行。
//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 =
4.Lockers 和 Condition
在多线程编程里面一个重要的概念是锁定,如果一个资源是多个线程共享的,为了保证数据的完整性,在进行事务性操作时需要将共享资源锁定,这样可以保证在做事务性操作时只有一个线程能对资源进行操作,从而保证数据的完整性。在 5.0 以前,锁定的功能是由 Synchronized 关键字来实现的,这样做存在 2 个问题:
1. 每次只能对一个对象进行锁定。若需要锁定多个对象,编程就比较麻烦,一不小心就会出现死锁现象。
2. 如果线程因拿不到锁定而进入等待状况,是没有办法将其打断的。
Lock 接口
ReentrantLock 是 Lock 的具体类, Lock 提供了以下一些方法:
lock(): 请求锁定,如果锁已被别的线程锁定,调用此方法的线程被阻断进入等待状态。
tryLock() :如果锁没被别的线程锁定,进入锁定状态,并返回 true 。若锁已被锁定,返回 false ,不进入等待状态。此方法还可带时间参数,如果锁在方法执行时已被锁定,线程将继续等待规定的时间,若还不行才返回 false 。
unlock() :取消锁定,需要注意的是 Lock 不会自动取消,编程时必须手动解锁。
1. // 生成一个锁
2. Lock lock = new ReentrantLock();
3. public void accessProtectedResource() {
4. lock.lock(); // 取得锁定
5. try {
6. // 对共享资源进行操作
7. } finally {
8. // 一定记着把锁取消掉,锁本身是不会自动解锁的
9. lock.unlock();
10. }
11. }
ReadWriteLock 接口
为了提高效率有些共享资源允许同时进行多个读的操作,但只允许一个写的操作,比如一个文件,只要其内容不变可以让多个线程同时读,不必做排他的锁定,排他的锁定只有在写的时候需要,以保证别的线程不会看到数据不完整的文件。 ReadWriteLock 可满足这种需要。 ReadWriteLock 内置两个 Lock ,一个是读的 Lock ,一个是写的 Lock 。多个线程可同时得到读的 Lock ,但只有一个线程能得到写的 Lock ,而且写的 Lock 被锁定后,任何线程都不能得到 Lock 。 ReadWriteLock 提供的方法有:
readLock(): 返回一个读的 lock
writeLock(): 返回一个写的 lock, 此 lock 是排他的。
1. public class FileOperator{
2. // 初始化一个 ReadWriteLock
3. ReadWriteLock lock = new ReentrantReadWriteLock();
4. public String read() {
5. // 得到 readLock 并锁定
6. Lock readLock = lock.readLock();
7. readLock.lock();
8. try {
9. // 做读的工作
10. return "Read something";
11. } finally {
12. readLock.unlock();
13. }
14. }
15.
16. public void write(String content) {
17. // 得到 writeLock 并锁定
18. Lock writeLock = lock.writeLock();
19. writeLock.lock();
20. try {
21. // 做读的工作
22. } finally {
23. writeLock.unlock();
24. } 25. } 26. }
虽然 ReadWriteLock 提供了一个高效的锁定机理,但最终程序的运行效率是和程序的设计息息相关的,比如说如果读的线程和写的线程同时在等待,要考虑是先释放读的 lock 还是先释放写的 lock 。如果写发生的频率不高,而且快,可以考虑先给写的 lock 。还要考虑的问题是如果一个写正在等待读完成,此时一个新的读进来,是否要给这个新的读发锁,如果释放了,可能导致写的线程等很久。
Condition 接口:
有时候线程取得 lock 后需要在一定条件下才能做某些工作,比如说经典的 Producer 和 Consumer 问题, Consumer 必须在队列里面有数据的时候才去消费数据,否则它必须暂时放弃对队列的锁定,等到 Producer 往队列里放了数据后再去拿来。而 Producer 必须等到队列有空闲了才能往里放数据,否则它也需要暂时解锁等 Consumer 把数据消费一些才能往队列里放数据。在 Java 5.0 以前,这种功能是由 Object 类的 wait(), notify() 和 notifyAll() 等方法实现的,在 5.0 里面,这些功能集中到了 Condition 这个接口来实现, Condition 提供以下方法:
await() :使调用此方法的线程放弃锁定,进入睡眠直到被打断或被唤醒。
signal(): 唤醒一个等待的线程
signalAll() :唤醒所有等待的线程
用一个简单的吃苹果例子来看看。
public class Basket {
Lock lock = new ReentrantLock();
// 产生 Condition 对象
Condition produced = lock.newCondition();
Condition consumed = lock.newCondition();
boolean available = false;
public void produce() throws InterruptedException {
lock.lock();
try {
if(available){
consumed.await(); // 放弃 lock 进入睡眠
}
/* 生产苹果 */
System.out.println("Apple produced.");
available = true;
produced.signal(); // 发信号唤醒等待这个 Condition 的线程
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
if(!available){
produced.await();// 放弃 lock 进入睡眠
}
/* 吃苹果 */
System.out.println("Apple consumed.");
available = false;
consumed.signal();// 发信号唤醒等待这个 Condition 的线程
} finally {
lock.unlock();
}
}
}
public class ConditionTester {
public static void main(String[] args) throws InterruptedException{
final Basket basket = new Basket();
// 定义一个 producer
Runnable producer = new Runnable() {
public void run() {
try {
basket.produce();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
};
// 定义一个 consumer
Runnable consumer = new Runnable() {
public void run() {
try {
basket.consume();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
};
// 各产生 10 个 consumer 和 producer
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0; i < 10; i++) {
service.submit(consumer);
}
Thread.sleep(2000);
for(int i=0; i<10; i++) {
service.submit(producer);
}
service.shutdown();
}