• 《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()方法处理未捕获的异常。这通常是在一组线程以同样方式处理未捕获异常时使用,若不同的线程需要有不同的异常处理方式,则最好在线程内部单独处理。

  • 相关阅读:
    随机变量与概率分布
    概率知识归纳
    随机生成&部门匹配
    SudokuGame 记软工第二次作业
    从0到1的开发,社交App 完成
    Emmm,从删库到跑路系列之.......Root权限的重要性
    处理AsyncTask的内存泄漏问题
    关于服务器端的Json文件的接收,踩了一早上的坑的问题
    一些安卓模拟器的IP问题和getOutputStream();关于connect();的函数异常的问题
    擦擦博客的灰------开始毕设,社交应用开发 之 前期准备
  • 原文地址:https://www.cnblogs.com/java-linux/p/9743375.html
Copyright © 2020-2023  润新知