您的位置:首页 > 编程语言 > Java开发

Java的并发

2015-10-03 10:08 459 查看

定义任务

线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来实现。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。如下LiftOff类的run()方法将显示倒计时:

public class LiftOff implements Runnable {
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++; // 标识符,final关键字一旦初始化就不再被修改

public LiftOff() {
}

public LiftOff(int countDown) {
this.countDown = countDown;
}

public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "),";
}

public void run() { // 通常run()被写成无限循环
while (countDown-- > 0) {
System.out.print(status());
Thread.yield(); // 静态方法,线程调度器
}
}
}


在下面的实例中,这个任务的run()方法不是由单独的线程操作的,它是在main方法中直接调用的:

public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run(); // 要实现线程行为必须显式地将一个任务附着到进程上
}
}
/*
* #0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(Liftoff!),
*/


Thread类

将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器,下面的实例展示了如何使用Thread来驱动LiftOff对象:

public class BasicThreads {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff()); // 构造器只需要一个Runnable对象
t.start(); // 调用对象的start方法执行初始化,调用run()方法
System.out.println("Waiting for LiftOff:");
}
}
/*
* Waiting for LiftOff:
* #0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(Liftoff!),
*/


尽管start()方法看起来是产生了一个对长期运行方法的调用,但是从输出中可以看到,start()迅速地返回了,因为Waiting for LiftOff消息在倒计时完成之前就出现了。实际上,你产生的是对LiftOff.run()的方法调用,并且这个方法还没有完成,但是因为Liftoff.run()是由不同的线程执行的,因此你仍旧可以执行main()线程中的其他操作(这种能力并不局限于main()线程,任何线程都可以启动另一个线程),因此,程序会同试运行两个方法,main()和LiftOff.run()是程序中与其他线程“同时”执行的的代码。
你可以很容易地添加更多进程去执行更多任务,下面,你可以看到所有任务彼此之间互相呼应:

public class MoreBasicThreads {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new LiftOff()).start();
}
System.out.println("Waiting for LiftOff:");
}
}
/*
Waiting for LiftOff:#4(9),#0(9),#3(9),#2(9),#1(9),#3(8),#0(8),#4(8),#3(7),#2(8),#1(8),#0(7),#4(7),#3(6),#2(7),#1(7),#0(6),#4(6),#3(5),#2(6),#1(6),#0(5),#4(5),#3(4),#2(5),#1(5),#0(4),#4(4),#3(3),#1(4),#2(4),#4(3),#0(3),#1(3),#3(2),#4(2),#0(2),#2(3),#1(2),#3(1),#4(1),#2(2),#0(1),#1(1),#3(Liftoff!),#2(1),#0(Liftoff!),#4(Liftoff!),#1(Liftoff!),#2(Liftoff!),
*/


输出说明不同任务的执行在线程被换进换出时混在了一起,这种交换是由线程调度器自动控制的。如果在你的机器上有多个处理器,线程调度器将会在这些处理器之间默默地分发线程。
上面这个程序一次运行的结果可能与另一次结果不同,因为线程调度机制是非确定性的。
当main()创建Thread对象时,它并没有捕获任何对这些对象的引用。在使用普通对象时,这对于垃圾回收来说是公平的,但是在使用Thread时,情况就不同了,每个Thread都“注册”了它自己,因此确实有一个对它的引用,并且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。你可以从输出中看到,这些任务确实运行到了结束。因此,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍然会继续存在。

使用Executor

Java的java.util.concurrent包中的执行器Executor将为你管理Thread对象,从而简化了并发编程。Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期。
我们可以使用Executor来代替在MoreBasicThreads.java中显式地创建Thread对象。LiftOff对象知道如何运行具体的任务,与命令设计模式一样,它暴露了要执行的单一方法。ExecutorService(具有服务生命周期的Executor)知道如何构建恰当的上下文来执行Runnable对象。在下面的实例中,CachedThreadPool将为每个任务都创建一个线程:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPool {
public static void main(String[] args) {
// ExecutorService对象时使用静态的Executor方法创建的,该方法可以确定其Executor类型
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();    //调用shutdown()方法可以防止新任务被提交给这个Executor
}
}
/*
#0(9),#2(9),#1(9),#0(8),#4(9),#2(8),#1(8),#0(7),#3(9),#4(8),#2(7),#1(7),#0(6),#3(8),#4(7),#2(6),#1(6),#0(5),#3(7),#4(6),#2(5),#1(5),#0(4),#3(6),#2(4),#4(5),#1(4),#3(5),#0(3),#2(3),#4(4),#3(4),#0(2),#1(3),#2(2),#4(3),#3(3),#0(1),#1(2),#2(1),#4(2),#3(2),#0(Liftoff!),#1(1),#2(Liftoff!),#4(1),#3(1),#1(Liftoff!),#4(Liftoff!),#3(Liftoff!),
*/


非常常见的情况是,单个的Executor被用来创建和管理系统中的所有任务。
可以很简单地将CachedThreadPool替换为不同类型的Executor。FixedThreadPool使用了有限的线程集来执行所提交的任务:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(5); // 限制线程数为5
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
/*
#0(9)
4000
,#2(9),#3(9),#1(9),#2(8),#4(9),#0(8),#3(8),#1(8),#4(8),#2(7),#0(7),#3(7),#1(7),#4(7),#2(6),#0(6),#3(6),#1(6),#4(6),#2(5),#0(5),#3(5),#1(5),#4(5),#2(4),#0(4),#3(4),#1(4),#4(4),#2(3),#0(3),#3(3),#1(3),#4(3),#2(2),#0(2),#3(2),#1(2),#4(2),#2(1),#0(1),#3(1),#1(1),#4(1),#2(Liftoff!),#0(Liftoff!),#3(Liftoff!),#1(Liftoff!),#4(Liftoff!),
*/


有了FixedThreadPool就可以一次性预先执行代价高昂的线程分配,因此也就可以限制线程的数量。这样可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器通过直接从池中获取线程,也可以如你所愿地尽快得到服务,FixedThreadPool使用的Thread对象的数量是有界的。
注意,在任何线程池中,现有线程在可能的情况下,都会被自动复用。
CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。
SingleThreadExecutor就像是线程数量为1的FixedThreadPool。这对于你希望在另一个线程中连续运行的任何事物来说,都是很有用的,例如监听进入的套接字连接的任务。它对于希望在线程中运行得短任务也同样很方便。
如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队。每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。在下面的示例中,你可以看到每个任务都是按照它们被提交的顺序,并且是在下一个任务开始之前完成的。因此,SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列。
作为另一个示例,假设你有大量的线程,那它们运行的任务将使用文件系统。你可以用SingleThreadExecutor来运行这些线程,以确保任何时刻在任何线程中都只有唯一的任务在运行。在这种方式中,你不需要在共享资源上处理同步(同时不会过度使用文件系统)。有时更好的解决方案是在资源上同步,但是SingleThreadExecutor可以让你省去只是为了维护某些事物的原型而进行的各种协调努力。通过序列化任务,你可以消除对序列化对象的需求。
 
参考书籍:《Thinking in Java》——Bruce Eckel
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 并发 线程