《Java 编程思想》读书笔记之并发(二)

基本的线程机制

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立的任务(也被称为子任务)中的每一个都将由「执行线程」来驱动。一个线程就是在进程中的一个单一的顺序控制流。

在使用线程时,CPU 将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用 CPU,但事实上 CPU 时间是划分成片段分配给了所有任务(此为单 CPU 情况,多 CPU 确实是同时执行)。

使用线程机制是一种建立透明的、可扩展的程序的方法,如果程序运行得太慢,为机器增添一个 CPU 就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理的方式。大数据的分布式扩展思想与之类似,当程序性能不行时,可以通过扩展集群提高程序并发度提高性能,但是不许修改代码。

定义任务

线程可以驱动任务,而描述任务需要一定的方式,java 中建议的方式是实现 Runnable 接口,其次是继承 Thread 类。以下是两种方式的代码实现:

Runnable 接口实现

public class MyTask implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + ": running...");
    }

    public static void main(String[] args) {
        Thread t = new Thread(new MyTask());
        t.start();
        System.out.println(Thread.currentThread() + ": running...");
    }

}

Thread 继承实现

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + ": running...");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println(Thread.currentThread() + ": running...");
    }

}

使用 Executor

Java SE5 的 java.util.concurrent 包中的执行器(Executor)将为你管理 Thread 对象,从而简化了并发编程。其实就是 Java 的线程池,它大大的减少的对于线程的管理,包括线程的创建、销毁等,并且它还能复用已经创建的线程对象,减少由于反复创建线程引起的开销,即节省了资源,同时也提高了程序的运行效率。这部分内容比较重要,之后会单独开一篇介绍,此处就介绍到这。

从任务中产生返回值

实现 Runnable 接口只能执行任务,无法获得任务的返回值。如果希望获得返回值,则应该实现 Callable 接口,并且应该使用 ExecutorService.submit() 方法调用它。下面是一个示例:

public class MyCallableTask implements Callable<String> {

    private int id;

    public MyCallableTask(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "Result : " + Thread.currentThread() + ": " + id;
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            futures.add(exec.submit(new MyCallableTask(i)));
        }

        for (Future<String> future : futures) {
            try {
                // 调用 future 方法会导致线程阻塞,直到 future 对应的线程执行完毕
                System.out.println(future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } finally {
                exec.shutdown();
            }
        }
    }

}

submit() 方法会产生 Future 对象,并且使用泛型的方式对返回值类型进行了定义。调用 Future.get() 方法,会导致线程阻塞,直到被调用的线程执行完毕返回结果,当然 java 也提供了设置超时时间的 get() 方法,防止线程一直阻塞,或者你也可以调用 Future 的 isDone() 方法预先判断线程是否执行完毕,再调用 get() 获取返回值。

休眠

「休眠」就是使任务中止执行指定的时间。在 Java 中可以通过Thread.sleep()方法来实现,JDK 1.6 之后推荐使用TimeUnit来实现任务的休眠。

sleep()方法的调用会抛出InterruptedException异常,由于异常不能跨线程传播,因此必须在本地处理任务内部产生的异常。

线程间虽然可以切换,但是并没有固定的顺序可言,因此,若要控制任务执行的顺序,绝对不要寄希望于线程的调度机制。

优先级

线程的优先级是用来控制线程的执行频率的,优先级高的线程执行频率高,但这并不会导致优先级低的线程得不到执行,仅仅是降低执行的频率。

在绝大多数时间里,所有线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误。——《Java 编程思想》

你可以在一个任务的内部,通过调用Thread.currentThread()来获得对驱动该任务的 Thread 对象的引用。

尽管 JDK 有 10 个优先级,但它与多数操作系统都不能映射得很好。因此在手动指定线程优先级的时候尽量只使用MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY三种级别。

让步

通过调用Thread.yield()方法可以使当前线程主动让出 CPU,同时向系统建议具有「相同优先级」的其他线程可以运行(只是一个建议,没有任何机制保证它一定会被采纳)。因此,对于任何重要的控制或在调整应用时,都不能依赖于yield()

后台线程

所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。——《Java 编程思想》

因此,当所有的非后台线程结束时,程序也就中止了,同时会杀死进程中的所有后台线程。

必须在线程启动之前调用setDeamon()方法,才能把它设置为后台线程。

可以通过调用isDaemon()方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程。

如果在后台线程中有finally{}语句块,当所有非后台程序执行结束时,后台线程会突然终止,并不会执行finally{}语句块中的内容,后台线程不会有任何开发者希望出现的结束确认形式。因为不能以优雅的方式来关闭后台线程,所以它们几乎不是一种好的思想。

加入一个线程

如果某个线程在另一个线程 t 上调用t.join(),此线程将被挂起,直到目标线程 t 结束才恢复(即t.isAlive()返回为 false)。也可以在调用join()时带上一个超时参数,在目标线程处理超时时join()方法总能返回。

join()方法的调用可以被中断,只要在调用线程上调用interrupt()方法。

捕获异常

由于线程的本质特性,一旦在run()方法中未捕捉异常,那么异常就会向外传播到控制台,除非采取特殊的步骤捕获这种错误的异常。

在 Java SE5 之前,可以使用线程组来捕获这些异常(不推荐),在 Java SE5 之后可以用 Executor 来解决这个问题。Executor 允许修改产生线程的方式,允许你在每个 Thread 对象上都附着一个异常处理器Thread.UncaughtExceptionHandler,此异常处理器会在线程因未捕获的异常而临近死亡时被调用uncaughtException()方法处理未捕获的异常。这通常是在一组线程以同样方式处理未捕获异常时使用,若不同的线程需要有不同的异常处理方式,则最好在线程内部单独处理。

posted @ 2018-10-04 20:55  牧码的羊  阅读(190)  评论(0编辑  收藏  举报