Java的并发

定义任务

线程可以驱动任务,因此你需要一种描述任务的方式,这可以由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),#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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值