java基础—多线程

1、线程概述

  几乎所有的操作系统都支持同时运行多个任务, 一个任务通常就 是一个程序,每个运行中的程序就是一个进程。当一个程序运行时, 内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

1.1、线程和进程

  几乎所有的操作系统都支持进程的概念, 所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
  一般而言,进程包含如下三个特征。
    ➢ 独立性:进程是系统中独立存在的实体,它可以拥有自己独立 的资源, 每一个进程都拥有自己私有的地址空间。 在没有经过 进程本身允许的情况下, 一个用户进程不可以直接访问其他进 程的地址空间。
    ➢ 动态性:进程与程序的区别在于,程序只是一个静态的指令集 合, 而进程是一个正在系统中活动的指令集合。 在进程中加入 了时间的概念。 进程具有自己的生命周期和各种不同的状态, 这些概念在程序中都是不具备的。
    ➢ 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
  并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行, 使得在宏观上具有多个进程同时执行的效果。
  大部分操作系统都支持多进程并发运行, 现代的操作系统几乎都支持同时运行多个任务。例如,程序员一边开着开发工具在写程序, 一边开着参考手册备查, 同时还使用电脑播放音乐……除此之外, 每台电脑运行时还有大量底层的支撑性程序在运行……这些进程看上去像是在同时工作。
  但事实的真相是,对于一个CPU而言,它在某个时间点只能执行一 个程序,也就是说,只能运行一个进程,CPU不断地在这些进程之间轮 换执行。那为什么用户感觉不到任何中断现象呢?这是因为CPU的执行 速度相对人的感觉来说实在是太快了(如果启动的程序足够多,用户 依然可以感觉到程序的运行速度下降),所以虽然CPU在多个进程之间 轮换执行,但用户感觉到好像有多个进程在同时执行。
  多线程则扩展了多进程的概念, 使得同一个进程可以同时并发处 理多个任务。 线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一 样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主 线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个 主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流 就是线程,每个线程也是互相独立的。
  线程是进程的组成部分, 一个进程可以拥有多个线程, 一个线程 必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和 自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该 进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因 、此编程更加方便;但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
  线程可以完成一定的任务, 可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。
  线程是独立运行的, 它并不知道进程中是否还有其他线程存在。 线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
  一个线程可以创建和撤销另一个线程, 同一个进程中的多个线程之间可以并发执行。
  从逻辑角度来看, 多线程存在于一个应用程序中, 让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成。
  简而言之, 一个程序运行后至少有一个进程, 一个进程里可以包含多个线程,但至少要包含一个线程。
  归纳起来可以这样说:操作系统可以同时执行多个任务,每个 任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

1.2、多线程的优势

  线程在程序中是独立的、并发的执行流, 与分隔的进程相比, 进 程中线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个 进程应有的状态。
 &emsp因为线程的划分尺度小于进程, 使得多线程程序的并发性高。 进 程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极 大地提高了程序的运行效率。
 &emsp线程比进程具有更高的性能, 这是由于同一个进程中的线程都有 共性——多个线程共享同一个进程虚拟空间。 线程共享的环境包括: 进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易 实现相互之间的通信。
 &emsp当操作系统创建一个进程时, 必须为该进程分配独立的内存空 间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用 多线程来实现并发比使用多进程实现并发的性能要高得多。
  总结起来,使用多线程编程具有如下几个优点。
    ➢ 进程之间不能共享内存,但线程之间共享内存非常容易。
    ➢ 系统创建进程时需要为该进程重新分配系统资源,但创建线程 则代价小得多, 因此使用多线程来实现多任务并发比多进程的 效率高。
    ➢ Java语言内置了多线程功能支持,而不是单纯地作为底层操作 系统的调度方式,从而简化了Java的多线程编程。

2、线程的创建和启动

  Java使用Thread类代表线程, 所有的线程对象都必须是Thread类 或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执 行一段程序流(一段顺序执行的代码)。 Java使用线程执行体来代表 这段程序流。

2.1、继承Thread类创建线程类

  通过继承Thread类来创建并启动多线程的步骤如下。
  ① 定义Thread类的子类,并重写该类的run()方法,该run()方法 的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执 行体。
  ② 创建Thread子类的实例,即创建了线程对象。
  ③ 调用线程对象的start()方法来启动该线程。下面程序示范了通过继承Thread类来创建并启动多线程。

// 通过继承Thread类来创建线程类
public class FirstThread extends Thread {
    private int i;

    // 重写run方法,run方法的方法体就是线程执行体
    @Override
    public void run() {
        for (; i < 100; i++) {
            // 当线程类继承Thread类时,直接使用this即可获取当前线程
            // Thread对象的getName()返回当前该线程的名字
            // 因此可以直接调用getName()方法返回当前线程的名
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName()+ " " + i);
            if (i == 20) {
                // 创建、并启动第一条线程
                new FirstThread().start();
                // 创建、并启动第二条线程
                new FirstThread().start();
            }
        }
    }
}

  运行效果如下图。
在这里插入图片描述
  进行多线程编程时不要忘记了Java程序运行时默认的主线程, main()方法的方法体就是主线程的线程执行体。
  虽然上面程序只显式地创建并启动了2个线程, 但实际上程序有3 个线程, 即程序显式创建的2个子线程和主线程。 前面已经提到, 当 Java程序开始运行后, 程序至少会创建一个主线程, 主线程的线程执 行体不是由run()方法确定的,而是由main()方法确定的—main()方法 的方法体代表主线程的线程执行体。
  除此之外,上面程序还用到了线程的如下两个方法。
    ➢ Thread.currentThread():currentThread()是Thread类的静
态方法,该方法总是返回当前正在执行的线程对象。
    ➢ getName():该方法是Thread类的实例方法, 该方法返回调用
该方法的线程名字。
  程序可以通过setName(String name)方法为线程设置名字, 也可以通过getName()方法返回指定线程的名字。在默认情况下,主线程的名字为main, 用户启动的多个线程的名字依次为Thread-0、 Thread-1、Thread-2、…、Thread-n,以此类推。
  从上图中可以看出, Thread-0和Thread-1两个线程输出的i变量不连续——注意:i变量是FirstThread的实例变量, 而不是局部变量, 但因为程序每次创建线程对象时都需要创建一个 FirstThread对象,所以Thread-0和Thread-1不能共享该实例变量。
  因此需要注意使用继承Thread类的方法来创建线程类时, 多个线程之间无法 共享线程类的实例变量。

2.2、实现Runnable接口创建线程类

  实现Runnable接口来创建并启动多线程的步骤如下。
  ① 定义Runnable接口的实现类, 并重写该接口的run()方法, 该 run()方法的方法体同样是该线程的线程执行体。
  ② 创建Runnable实现类的实例,并以此实例作为Thread的target 来创建Thread对象,该Thread对象才是真正的线程对象。代码如下:

	// 创建 Runnable实现类的对象 
	SecondThread st = new SecondThread();
	// 以Runnable实现类的对象作为Thread的target来创建 Thread对象,即线程对象 
	new Thread(st);
	// 也可在创建 hread对象时指定target和新线程的名字 
	new Thread(st, "新线程1");

  ③ 调用线程对象的start()方法来启动该线程。
  从上述可知,Runnable对象仅仅作为Thread对象的target, Runnable实现类 里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是 Thread实例,只是该Thread线程负责执行其target的run()方法。
  下面程序示范了通过实现Runnable接口来创建并启动多线程。

// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable {
    private int i;

    // run方法同样是线程执行体
    public void run() {
        for (; i < 100; i++) {
            // 当线程类实现Runnable接口时,
            // 如果想获取当前线程,只能用Thread.currentThread()方法。
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()
                    + " " + i);
            if (i == 20) {
                SecondThread st = new SecondThread();     // ①
                // 通过new Thread(target, name)方法创建新线程
                new Thread(st, "新线程1").start();
                new Thread(st, "新线程2").start();
            }
        }
    }
}

  对 比 FirstThread 中 的 run() 方 法 体 和 SecondThread中的run()方法体不难发现,通过继承Thread类来获得当 前线程对象比较简单, 直接使用this就可以了;但通过实现Runnable 接口来获得当前线程对象, 则必须使用Thread.currentThread()方法。
  Runnable 接 口 中 只 包 含 一 个 抽 象 方 法 , 从 Java 8 开 始 , Runnable 接 口 使 用 了 @FunctionalInterface 修 饰 。 也 就 是 说 , Runnable接口是函数式接口, 可使用Lambda表达式创建Runnable对 象。接下来介绍的Callable接口也是函数式接口。
  运行上面程序,效果如下图。
在这里插入图片描述
  从上图中可以看出,两个子线程的i变量是连续的, 也就是采用Runnable接口的方式创建的多个线程可以共享线 程类的实例变量。 这是因为在这种方式下, 程序所创建的Runnable对 象只是线程的target, 而多个线程可以共享同一个target, 所以多个 线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。

2.3、使用Callable和Future创建线程

  从Java 5开始,Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版, Callable接口提供了一个 call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
  ➢ call()方法可以有返回值。
  ➢ call()方法可以声明抛出异常。
  因此完全可以提供一个Callable对象作为Thread的target, 而该线程的线程执行体就是该Callable对象的call()方法。 但问题是: Callable接口是Java 5新增的接口, 而且它不是Runnable接口的子接 口, 所以Callable对象不能直接作为Thread的target。 而且call()方 法还有一个返回值——call()方法并不是直接调用, 它是作为线程执 行体被调用的。那么如何获取call()方法的返回值呢?
  Java 5提供了Future接口来代表Callable接口里call()方法的返回值, 并为Future接口提供了一个FutureTask实现类, 该实现类实现了Future 接 口, 并实现了Runnable接口 —— 可 以作 为 Thread类 的 target。
  在 Future 接 口 里 定 义 了 如 下 几 个 公 共 方 法 来 控 制 它 关 联 的 Callable任务。
    ➢ boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
    ➢ V get():返回Callable任务里call()方法的返回值。 调用该 方法将导致程序阻塞, 必须等到子线程结束后才会得到返回 值。
    ➢ V get(long timeout, TimeUnit unit):返回Callable任务里 call()方法的返回值。该方法让程序最多阻塞timeout和unit指 定的时间, 如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
    ➢ boolean isCancelled():如果在Callable任务正常完成前被 取消,则返回true。
    ➢ boolean isDone():如果Callable任务已完成,则返回true。
  创建并启动有返回值的线程的步骤如下。
  ① 创建Callable接口的实现类,并实现call()方法,该call()方 法将作为线程执行体, 且该call()方法有返回值, 再创建Callable实 现 类 的 实 例 。 从 Java 8 开 始 ,可 以 直 接 使 用 Lambda 表 达 式 创 建 Callable对象。
  ② 使用FutureTask类来包装Callable对象,该FutureTask对象封 装了该Callable对象的call()方法的返回值。
  ③ 使用FutureTask对象作为Thread对象的target创建并启动新线 程。
  ④ 调用FutureTask对象的get()方法来获得子线程执行结束后的 返回值。
  下面程序通过实现Callable接口来实现线程类,并启动该线程。

public class ThirdThread {
    public static void main(String[] args) {
        // 创建Callable对象
        ThirdThread rt = new ThirdThread();
        // 先使用Lambda表达式创建Callable<Integer>对象
        // 使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<>(() -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            }
            // call()方法可以有返回值
            return i;
        });

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            if (i == 20) {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
            // 获取线程返回值
            System.out.println("子线程的返回值:" + task.get());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

  上面程序中使用Lambda表达式直接创建了Callable对象, 这样就 】无须先创建Callable实现类, 再创建Callable对象了。 实现Callable 接口与实现Runnable接口并没有太大的差别, 只是Callable的call() 方法允许声明抛出异常,而且允许带返回值。
  运行上面程序, 将看到主线程和call()方法所代表的线程交替执 行的情形,程序最后还会输出call()方法的返回值。

2.4、创建线程的三种方式对比

  采用实现Runnable、Callable接口的方式创建多线程的优缺点:
  ➢ 线程类只是实现了Runnable接口或Callable接口,还可以继承 、其他类。
  ➢ 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  ➢ 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
  采用继承Thread类的方式创建多线程的优缺点:
  ➢ 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
  ➢ 优势是,编写简单 如果需要访问当前线程, 则无须使用 Thread.currentThread()方法, 直接使用this即可获得当前线程。
  鉴于上面分析, 因此一般推荐采用实现Runnable接口、Callable 接口的方式来创建多线程。

3、线程的生命周期

  当线程被创建并启动以后, 它既不是一启动就进入了执行状态, 也不是一直处于执行状态, 在线程的生命周期中, 它要经过新建 (New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死 亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占” 着CPU独自运行, 所以CPU需要在多条线程之间切换, 于是线程状态也会多次在运行、就绪之间切换。

3.1、新建和就绪状态

  当程序使用new关键字创建了一个线程之后,该线程就处于新建状 态, 此时它和其他的Java对象一样, 仅仅由Java虚拟机为其分配内 存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程 的动态特征,程序也不会执行线程的线程执行体。
  当线程对象调用了start()方法之后,该线程处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程 并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始 运行,取决于JVM里线程调度器的调度。
  启动线程使用start()方法, 而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程, 系统会把该 run()方法当成线程执行体来处理。

public class InvokeRun extends Thread {
    private int i;

    // 重写run方法,run方法的方法体就是线程执行体
    public void run() {
        for (; i < 100; i++) {
            // 直接调用run方法时,Thread的this.getName返回的是该对象名字,
            // 而不是当前线程的名字。
            // 使用Thread.currentThread().getName()总是获取当前线程名字
            System.out.println(Thread.currentThread().getName() + " " + i);   // ①
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName()
                    + " " + i);
            if (i == 20) {
                // 直接调用线程对象的run方法,
                // 系统会把线程对象当成普通对象,run方法当成普通方法,
                // 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
                new InvokeRun().run();
                new InvokeRun().run();
            }
        }
    }
}

  上面程序创建线程对象后直接调用了线程对象的run()方法, 程序运行的结果是整个程序只有一个线程:主线程。
  通过上面程序不难看出, 启动线程的正确方法是调用Thread对象 的start()方法, 而不是直接调用run()方法, 否则就变成单线程程序了。
  需要指出的是,调用了线程的run()方法之后,该线程已经不再处 于新建状态,不要再次调用线程对象的start()方法。因为只能对处于新建状态的线程调用start()方法, 否则将引发 IllegalThreadStateException异常。

3.2、运行和阻塞状态

  如果处于就绪状态的线程获得了CPU, 开始执行run()方法的线程 执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任 何时刻只有一个线程处于运行状态。 当然, 在一个多处理器的机器 上, 将会有多个线程并行(注意是并行:parallel)执行;当线程数 大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
  当一个线程开始运行后, 它不可能一直处于运行状态(除非它的 线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被 中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底 层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每 个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统 就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择 下一个线程时,系统会考虑线程的优先级。
  所有现代的桌面和服务器操作系统都采用抢占式调度策略, 但一 些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只 有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的 资源——也就是必须由该线程主动放弃所占用的资源。
  当发生如下情况时,线程将会进入阻塞状态。
  ➢ 线程调用sleep()方法主动放弃所占用的处理器资源。
  ➢ 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  ➢ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将有更深入的介绍。
  ➢ 线程在等待某个通知(notify)。
  ➢ 程序调用了线程的suspend()方法将该线程挂起。 但这个方法容易导致死锁,所以应该尽量避免使用该方法。

  当前正在执行的线程被阻塞之后, 其他线程就可以获得执行的机 会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状 态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新 等待线程调度器再次调度它。
  针对上面几种情况, 当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。
  ➢ 调用sleep()方法的线程经过了指定时间。
  ➢ 线程调用的阻塞式IO方法已经返回。
  ➢ 线程成功地获得了试图取得的同步监视器。
  ➢ 线程正在等待某个通知时,其他线程发出了一个通知。
  ➢ 处于挂起状态的线程被调用了resume()恢复方法。
  下图显示了线程状态转换图。
在这里插入图片描述
  从上图中可以看出, 线程从阻塞状态只能进入就绪状态, 无法直接进入运行状态。 而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器 资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资 源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。关于yield()方法后面有更详细的介绍。

3.3、线程死亡

  线程会以如下三种方式结束,结束后就处于死亡状态。
  ➢ run()或call()方法执行完成,线程正常结束。
  ➢ 线程抛出一个未捕获的Exception或Error。
  ➢ 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
  需要注意的是当主线程结束时,其他线程不受任何影响,并不会随之结束。 一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。
  为了测试某个线程是否已经死亡,可以调用线程对象的isAlive() 方法,当线程处于就绪、运行、阻塞三种状态时, 该方法将返回 true;当线程处于新建、死亡两种状态时,该方法将返回false。
  不要试图对一个已经死亡的线程调用start()方法使它重新启 动,死亡就是死亡,该线程将不可再次作为线程执行。
  下面程序尝试对处于死亡状态的线程再次调用start()方法。

public class StartDead extends Thread {
    private int i;

    // 重写run方法,run方法的方法体就是线程执行体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        // 创建线程对象
        StartDead sd = new StartDead();
        for (int i = 0; i < 300; i++) {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20) {
                // 启动线程
                sd.start();
                // 判断启动后线程的isAlive()值,输出true
                System.out.println(sd.isAlive());
            }
            // 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
            // 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
            // 那只能是死亡状态了。
            if (i > 20 && !sd.isAlive()) {
                // 试图再次启动该线程
                sd.start();
            }
        }
    }
}

  上面程序中的代码试图在线程已死亡的情况下再次调用 start() 方 法 来 启 动 该 线 程 。 运行上面程序 ,将 引 发 IllegalThreadStateException异常,这表明处于死亡状态的线程无法再次运行了。
  不要对处于死亡状态的线程调用start()方法,程序只能对新建 状态的线程调用start()方法, 对新建状态的线程两次调用start() 方法也是错误的。 这都会引发IllegalThreadState Exception异常。

4、控制线

4.1、join线程

  Thread提供了让一个线程等待另一个线程完成的方法——join() 方法。 当在某个程序执行流中调用其他线程的join()方法时, 调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
  join()方法通常由使用线程的程序调用, 以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后, 再调用主线程来进一步操作。

public class JoinThread extends Thread {
    // 提供一个有参数的构造器,用于设置该线程的名字
    public JoinThread(String name) {
        super(name);
    }

    // 重写run()方法,定义线程执行体
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) throws Exception {
        // 启动子线程
        new JoinThread("新线程").start();
        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                JoinThread jt = new JoinThread("被Join的线程");
                jt.start();
                // main线程调用了jt线程的join()方法,main线程必须等jt执行结束才会向下执行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

  上面程序中一共有3个线程, 主方法开始时就启动了名为“新线 程”的子线程, 该子线程将会和main线程并发执行。 当主线程的循环 变量i等于20时,启动了名为“被Join的线程”的线程,该线程不会和 main线程并发执行, main线程必须等该线程执行结束后才可以向下执 行。在名为“被Join的线程”的线程执行时,实际上只有2个子线程并发执行, 而主线程处于等待状态。运行上面程序,运行效果如下图。
在这里插入图片描述
  从上图中可以看出,主线程执行到i==20时,程序启动并join了 名为“被Join的线程”的线程, 所以主线程将一直处于阻塞状态, 直 到名为“被Join的线程”的线程执行完成。
  join()方法有如下三种重载形式
  ➢ join():等待被join的线程执行完成。
  ➢ join(long millis):等待被join的线程的时间最长为millis 毫秒。 如果在millis毫秒内被join的线程还没有执行结束, 则 不再等待。
  ➢ join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫微秒。
  通常很少使用第三种形式,原因有两个:程序对时间的精度无 须精确到毫微秒;计算机硬件、操作系统本身也无法精确到毫微 秒。

4.2、后台线程

  有一种线程, 它是在后台运行的, 它的任务是为其他的线程提供 服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守 护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
  后台线程有个特征:如果所有的前台线程都死亡, 后台线程会自动死亡。
  调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。下面程序将执行线程设置成后台线程,可以看到当所有的前台线程死亡时, 后台线程随之死亡。 当整个虚拟机中只剩下后台线程 时,程序就没有继续运行的必要了,所以虚拟机也就退出了。

public class DaemonThread extends Thread {
    // 定义后台线程的线程执行体与普通线程没有任何区别
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {
        DaemonThread t = new DaemonThread();
        // 将此线程设置成后台线程
        t.setDaemon(true);
        // 启动后台线程
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
        // -----程序执行到此处,前台线程(main线程)结束------
        // 后台线程也应该随之结束
    }
}

  上面程序中的先将t线程设置成后台线程,然后启动该线程,本来该线程应该执行到i等于999时才会结束,但运行程序时不难发现该后台线程无法运行到999,因为当主线程也就是程序中唯一的 前台线程运行结束后,JVM会主动退出,因而后台线程也就被结束了。运行结果如下图。
在这里插入图片描述
  从运行结果可以看出后台线程(Thread-0)不是立即死亡,还运行输出了一会儿,这是因为前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必 须在该线程启动之前设置, 也就是说, setDaemon(true)必须在 start()方法之前调用, 否则会引发IllegalThreadStateException 异常。
  主线程默认是前台线程,t线程默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认就是后 台线程——前台线程创建的子线程默认是前台线程, 后台线程创建的 子线程默认是后台线程。

4.3、线程睡眠:sleep和线程插队:yield

  如果需要让当前正在执行的线程暂停一段时间, 并进入阻塞状态, 则可以通过调用Thread类的静态sleep()方法来实现。 sleep()方 法有两种重载形式。
  ➢ static void sleep(long millis):让当前正在执行的线程暂 停millis毫秒, 并进入阻塞状态, 该方法受到系统计时器和线 程调度器的精度与准确度的影响。
  ➢ static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态, 该方法受到系统计时器和线程调度器的精度与准确度的影响。
  与前面类似的是,程序很少调用第二种形式的sleep()方法。
  当当前线程调用sleep()方法进入阻塞状态后, 在其睡眠时间段 内, 该线程不会获得执行的机会, 即使系统中没有其他可执行的线 程, 处于sleep()中的线程也不会执行, 因此sleep()方法常用来暂停程序的执行。 下面程序调用sleep()方法来暂停主线程的执行,因为该程序只有一个主线程,当主线程进入睡眠后,系统没有可执行的线程,所以可 以看到程序在sleep()方法处暂停。

public class SleepTest {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("当前时间: " + new Date());
            // 调用sleep方法让当前线程暂停1s。
            Thread.sleep(3000);
        }
    }
}

  上面程序中的代码将当前执行的线程暂停3秒,运行上面程序,看到程序依次输出10条字符串,输出2条字符串之间的时间间隔为3秒。
  此外, Thread还提供了一个与sleep()方法有点相似的yield()静态方法, 它也可以让当前正在执行的线程暂停, 但它不会阻塞该线 程, 它只是将该线程转入就绪状态。 yield()只是让当前线程暂停一 下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
  实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
  关于sleep()方法和yield()方法的区别如下。
  ➢ sleep()方法暂停当前线程后, 会给其他线程执行机会, 不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
  ➢ sleep()方法会将线程转入阻塞状态, 直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield() 方法暂停之后,立即再次获得处理器资源被执行。
  ➢ sleep()方法声明抛出了InterruptedException异常, 所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常; 而yield()方法则没有声明抛出任何异常。
  ➢ sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

4.4、改变线程优先级

  每个线程执行时都具有一定的优先级, 优先级高的线程获得较多 的执行机会,而优先级低的线程则获得较少的执行机会。
  每个线程默认的优先级都与创建它的父线程的优先级相同, 在默 认情况下, main线程具有普通优先级, 由main线程创建的子线程也具 有普通优先级。
  Thread 类 提 供 了 setPriority ( int newPriority ) 、 getPriority() 方 法 来 设 置 和 返 回 指 定 线 程 的 优 先 级 , 其 中 setPriority()方法的参数可以是一个整数, 范围是1~10之间, 也可以使用Thread类的如下三个静态常量。
    ➢ MAX_PRIORITY:其值是10。
    ➢ MIN_PRIORITY:其值是1。
    ➢ NORM_PRIORITY:其值是5。
  下面程序使用了setPriority()方法来改变主线程的优先级,并使用该方法改变了两个线程的优先级,从而可以看到高优先级的线程将会获得更多的执行机会。

public class PriorityTest extends Thread {
    // 定义一个有参数的构造器,用于创建线程时指定name
    public PriorityTest(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName() + ",其优先级是:" + getPriority() + ", 循环变量的值为:" + i);
        }
    }

    public static void main(String[] args) {
        // 改变主线程的优先级
        Thread.currentThread().setPriority(6);
        for (int i = 0; i < 30; i++) {
            if (i == 10) {
                PriorityTest low = new PriorityTest("低级");
                low.start();
                System.out.println("创建之初的优先级:" + low.getPriority());
                // 设置该线程为最低优先级
                low.setPriority(Thread.MIN_PRIORITY);
            }

            if (i == 20) {
                PriorityTest high = new PriorityTest("高级");
                high.start();
                System.out.println("创建之初的优先级:" + high.getPriority());
                // 设置该线程为最高优先级
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}

  上面程序中的代码先改变了主线程的优先级为6,这样由main线程所创建的子线程的优先级默认都是6, 所以程序直接输出 low、high两个线程的优先级时应该看到6。接着程序将low线程的优先 级 设 为 Priority.MIN_PRIORITY , 将 high 线 程 的 优 先 级 设 置 为 Priority.MAX_PRIORITY。
  值得指出的是, 虽然Java提供了10个优先级级别, 但这些优先级 级别需要操作系统的支持。遗憾的是,不同操作系统上的优先级并不 相同, 而且也不能很好地和Java的10个优先级对应, 例如Windows 2000仅提供了7个优先级。因此应该尽量避免直接为线程指定优先级, 而应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常 量来设置优先级,这样才可以保证程序具有最好的可移植性。

5、线程同步

  多线程编程是有趣的事情, 它很容易突然出现“错误情况”, 这 是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出 现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个 数据时,很容易“偶然”出现线程安全问题。

5.1、线程安全问题

  关于线程安全问题, 有一个经典的问题——银行取钱的问题。 银行取钱的基本流程基本上可以分为如下几个步骤。
  ① 用户输入账户、密码,系统判断用户的账户、密码是否匹配。
  ② 用户输入取款金额。
  ③ 系统判断账户余额是否大于取款金额。
  ④ 如果余额大于取款金额, 则取款成功;如果余额小于取款金 额,则取款失败。
  乍一看上去, 这个流程确实就是日常生活中的取款流程, 这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!
  按上面的流程去编写取款程序, 并使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。此处忽略检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个实例变量。

public class Account {
    // 封装账户编号、账户余额的两个成员变量
    private String accountNo;
    private double balance;

    public Account() {
    }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountNo() {
        return this.accountNo;
    }

    // balance的setter和getter方法
    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double getBalance() {
        return this.balance;
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

  接下来提供一个取钱的线程类, 该线程类根据执行账户、取钱数 量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余 额足够时系统吐出钞票,余额减少。

public class DrawThread extends Thread {
    // 模拟用户账户
    private Account account;
    // 当前取钱线程所希望取的钱数
    private double drawAmount;

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
    @Override
    public void run() {
        // 账户余额大于取钱数目
        if (account.getBalance() >= drawAmount) {
            // 吐出钞票
            System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
//            try {
//                Thread.sleep(1);
//            } catch (InterruptedException ex) {
//                ex.printStackTrace();
//            }
            // 修改余额
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("\t余额为: " + account.getBalance());
        } else {
            System.out.println(getName() + "取钱失败!余额不足!");
        }
    }
}

  先不要管程序中那段被注释掉的代码, 上面程序是一 个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。 程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱。程序如下。

public class DrawTest {
    public static void main(String[] args) {
        // 创建一个账户
        Account acct = new Account("1234567", 1000);
        // 模拟两个线程对同一个账户取钱
        new DrawThread("甲", acct, 800).start();
        new DrawThread("乙", acct, 800).start();
    }
}

  多次运行上面程序, 很有可能都会看到如下图所示的错误结果。
在这里插入图片描述
  如上图所示的运行结果并不是银行所期望的结果(不过有可能看到运行正确的效果),这正是多线程编程突然出现的“偶然”错误 ——因为线程调度的不确定性。 让另一个线程执行——为了强制暂停, 只要取消上面程序中被注释的代码即可。取消注释后再次编译DrawThread.java,并再次 运行DrawTest类,将总可以看到上图的错误结果。

5.2、同步代码块

  之所以出现之前扣款错误的结果,是因为run()方法的方法体不具有同步安全性——程序中有两个并发线程在修改Account对象;而且系统恰好在修改余额操作处执行了线程切换,切换给另一个修改Account对象 的线程,所以就出现了问题。
  为了解决这个问题, Java的多线程支持引入了同步监视器来解决 这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块 的语法格式如下:

    synchronized (obj) {
    	...
        //此处的代码就是同步代码块
    }

  任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
  虽然Java程序允许使用任何对象作为同步监视器, 但想一下同步 监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此 通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面 的取钱模拟程序,应该考虑使用账户(account)作为同步监视器,把 程序修改成如下形式。

public class DrawThread extends Thread {
    // 模拟用户账户
    private final Account account;
    // 当前取钱线程所希望取的钱数
    private final double drawAmount;

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
    @Override
    public void run() {
        // 使用account作为同步监视器,任何线程进入下面同步代码块之前,
        // 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
        // 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
        synchronized (account) {
            // 账户余额大于取钱数目
            if (account.getBalance() >= drawAmount) {
                // 吐出钞票
                System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                // 修改余额
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t余额为: " + account.getBalance());
            } else {
                System.out.println(getName() + "取钱失败!余额不足!");
            }
        }
        // 同步代码块结束,该线程释放同步锁
    }
}

  上面程序使用synchronized将run()方法里的方法体修改成同步代 码块, 该同步代码块的同步监视器是account对象, 这样的做法符合 “加锁→修改→释放锁”的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。
  将DrawThread修改为上面所示的情形之后, 多次运行该程序, 总可以看到如下图所示的正确结果。
在这里插入图片描述

5.3、同步方法

  与同步代码块对应, Java的多线程安全支持还提供了同步方法, 同步方法就是使用synchronized关键字来修饰某个方法, 则该方法称为同步方法。 对于synchronized修饰的实例方法(非static方法)而言, 无须显式指定同步监视器, 同步方法的同步监视器是this, 也就是调用该方法的对象。
  通过使用同步方法可以非常方便地实现线程安全的类, 线程安全的类具有如下特征。
  ➢ 该类的对象可以被多个线程安全地访问。
  ➢ 每个线程调用该对象的任意方法之后都将得到正确结果。
  ➢ 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
  前面介绍了可变类和不可变类, 其中不可变类总是线程安全的, 因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线 程安全。 例如上面的Account就是一个可变类, 它的accountNo和 balance两个成员变量都可以被改变, 当两个线程同时修改Account对 象的balance成员变量的值时, 程序就出现了异常。 下面将Account类 对balance的访问设置成线程安全的, 那么只要把修改balance的方法 变成同步方法即可。程序如下。

public class Account {
    // 封装账户编号、账户余额的两个成员变量
    private String accountNo;
    private double balance;

    public Account() {
    }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountNo() {
        return this.accountNo;
    }

    // balance的setter和getter方法
    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double getBalance() {
        return this.balance;
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }

    // 提供一个线程安全draw()方法来完成取钱操作
    public synchronized void draw(double drawAmount) {
        // 账户余额大于取钱数目
        if (balance >= drawAmount) {
            // 吐出钞票
            System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount);
            try {
                Thread.sleep(1);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            // 修改余额
            balance -= drawAmount;
            System.out.println("\t余额为: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!");
        }
    }
}

  上 面 程 序 中 增 加 了 一 个 代 表 取 钱 的 draw() 方 法 , 并 使 用 了 synchronized关键字修饰该方法(synchronized关键字可以修饰方法, 可以修饰代码块, 但不能修饰构造器、成员变量等), 把该方法变成同步方法, 该同步方 法的同步监视器是this,因此对于同一个Account账户而言,任意时刻 只能有一个线程获得对Account对象的锁定, 然后进入draw ()方法执 行取钱操作——这样也可以保证多个线程并发取钱的线程安全。
  因为Account 类 中 已 经 提 供 了 draw() 方 法 , 而 且 取 消 了 setBalance()方法,DrawThread线程类需要改写,该线程类的run()方 法只要调用Account对象的draw()方法即可执行取钱操作。 run()方法代码片段如下:

public class DrawThread extends Thread {
    // 模拟用户账户
    private final Account account;
    // 当前取钱线程所希望取的钱数
    private final double drawAmount;

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    @Override
    public void run() {
        // 直接调用account对象的draw方法来执行取钱
        // 同步方法的同步监视器是this,this代表调用draw()方法的对象。
        // 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
        account.draw(drawAmount);
    }
}

  上面的DrawThread类无须自己实现取钱操作, 而是直接调用 account的draw()方法来执行取钱操作。由于已经使用synchronized关 键字修饰了draw()方法, 同步方法的同步监视器是this, 而this总代 表调用该方法的对象——在上面示例中, 调用draw()方法的对象是 account, 因此多个线程并发修改同一份account之前, 必须先对 account对象加锁。这也符合了“加锁 → 修改 → 释放锁”的逻辑。
  可变类的线程安全是以降低程序的运行效率作为代价的, 为了减 少线程安全所带来的负面影响,程序可以采用如下策略。
  ➢ 不要对线程安全类的所有方法都进行同步,只对那些会改变竞 争资源(竞争资源也就是共享资源)的方法进行同步。 例如上 面Account类中的accountNo实例变量就无须同步, 所以程序只 对draw()方法进行了同步控制。
  ➢ 如果可变类有两种运行环境:单线程环境和多线程环境,则应 该为该可变类提供两种版本, 即线程不安全版本和线程安全版 本。 在单线程环境中使用线程不安全版本以保证性能, 在多线程环境中使用线程安全版本。
  例如JDK所提供的StringBuilder、StringBuffer就是为了照顾单线 程环境 和 多线 程 环 境 所提 供 的 类 。

5.4、释放同步监视器的锁定

  任何线程进入同步代码块、同步方法之前, 必须先获得对同步监 视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式 释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视 器的锁定。
  ➢ 当前线程的同步方法、同步代码块执行结束,当前线程即释放 同步监视器。
  ➢ 当前线程在同步代码块、同步方法中遇到break、return终止 了该代码块、该方法的继续执行, 当前线程将会释放同步监视 器。
  ➢ 当前线程在同步代码块、同步方法中出现了未处理的Error或 Exception,导致了该代码块、该方法异常结束时,当前线程将 会释放同步监视器。
  ➢ 当前线程执行同步代码块或同步方法时,程序执行了同步监视 器对象的wait()方法,则当前线程暂停,并释放同步监视器。 在如下所示的情况下,线程不会释放同步监视器。
  ➢ 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
  ➢ 线程执行同步代码块时, 其他线程调用了该线程的suspend() 方法将该线程挂起, 该线程不会释放同步监视器。 当然, 程序 应该尽量避免使用suspend()和resume()方法来控制线程。

5.5、同步锁(Lock)

  从Java 5开始, Java提供了一种功能更强大的线程同步机制—通过显式定义同步锁对象来实现同步, 在这种机制下, 同步锁由Lock 对象充当。
  Lock提供了比synchronized方法和synchronized代码块更广泛的 锁定操作, Lock允许实现更灵活的结构, 可以具有差别很大的属性, 并且支持多个相关的Condition对象。
  Lock是控制多个线程对共享资源进行访问的工具。 通常, 锁提供了对共享资源的独占访问, 每次只能有一个线程对Lock对象加锁, 线 程开始访问共享资源之前应先获得Lock对象。
  某些锁可能允许对共享资源并发访问, 如ReadWriteLock(读写 锁),Lock、ReadWriteLock是Java 5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类, 为ReadWriteLock提供了 ReentrantReadWriteLock实现类。
  Java 8新增了新型的StampedLock类,在大多数场景中它可以替代 传统的ReentrantReadWriteLock。 ReentrantReadWriteLock为读写操 作提供了三种锁模式:Writing、ReadingOptimistic、Reading。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁 ) 。
  使 用 该 Lock 对 象 可 以 显 式 地 加 锁 、 释 放 锁 ,使 用 该 Lock 对 象 可 以 显 式 地 加 锁 、 释 放 锁 , 通 常 使 用 ReentrantLock的代码格式如下:

public class test {
    // 定义锁对象
    private final ReentrantLock lock = new ReentrantLock();

	public void m(){
		// 加锁
        lock.lock();
        try {
        	// 业务逻辑
        	...
        } finally {
            // 修改完成,释放锁
            lock.unlock();
        }
	}
}

  使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。通过 使用ReentrantLock对象, 可以把Account类改为如下形式, 它依然是线程安全的。

public class Account {
    // 定义锁对象
    private final ReentrantLock lock = new ReentrantLock();

    // 封装账户编号、账户余额的两个成员变量
    private String accountNo;
    private double balance;

    public Account() {
    }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountNo() {
        return this.accountNo;
    }

    // balance的setter和getter方法
    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double getBalance() {
        return this.balance;
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }

    // 提供一个线程安全draw()方法来完成取钱操作
    public synchronized void draw(double drawAmount) {
        // 加锁
        lock.lock();
        try {
            // 账户余额大于取钱数目
            if (balance >= drawAmount) {
                // 吐出钞票
                System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                // 修改余额
                balance -= drawAmount;
                System.out.println("\t余额为: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!");
            }
        } finally {
            // 修改完成,释放锁
            lock.unlock();
        }
    }
}

  上面程序中的第一行定义了一个ReentrantLock对象, 程 序 中 实 现 draw() 方 法 时 , 进 入 方 法 开 始 执 行 后 立 即 请 求 对 ReentrantLock对象进行加锁,当执行完draw()方法的取钱逻辑之后, 程序使用finally块来确保释放锁。
  使用Lock与使用同步方法有点相似, 只是使用Lock时显式使用 Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作 为同步监视器,同样都符合“加锁→修改→释放锁”的操作模式, 而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以 保证对于同一个Account对象,同一时刻只能有一个线程能进入临界区。
  同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视 器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取 了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取 时相同的范围内释放所有锁。
  虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以 更为灵活的方式使用锁。 Lock提供了同步方法和同步代码块所没有的 其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly() 方 法 , 还 有 获 取 超 时 失 效 锁 的 tryLock(long,TimeUnit)方法。
  ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁, ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁, 所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

5.6、死锁及常用处理策略

  当两个线程相互等待对方释放同步监视器时就会发生死锁, Java 虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程 时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生 任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
  死锁是很容易发生的, 尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。

class A {
    public synchronized void foo(B b) {
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo()方法");     // ①
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last()方法");    // ③
        b.last();
    }

    public synchronized void last() {
        System.out.println("进入了A类的last()方法内部");
    }
}

class B {
    public synchronized void bar(A a) {
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar()方法");   // ②
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last()方法");  // ④
        a.last();
    }

    public synchronized void last() {
        System.out.println("进入了B类的last()方法内部");
    }
}


public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    @Override
    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        // 以dl为target启动新线程
        new Thread(dl).start();
        // 调用init()方法
        dl.init();
    }
}

  运行上面程序,将会卡主无法向下执行,如下图所示。
在这里插入图片描述
  死锁是不应该出现在程序中的, 编写程序时应该尽量避免死锁。 可以通过下面几种常见方式来解决死锁问题。
  ➢ 避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定。 比如上面的死锁程序, 主线程要对A、B两个对象(同步监视器)进行锁定, 副线程也要对A、B两个对象进行锁定, 这就埋下了导致死锁的隐患。
  ➢ 具有相同的加锁顺序:如果多个线程需要对多个同步监视器进行锁定, 则应该保证它们以相同的顺序请求加锁。 比如上面的 死锁程序, 主线程先对A对象(同步监视器)加锁, 再对B对象 (同步监视器)加锁;而副线程则先对B对象加锁, 再对A对象加锁。 这种方式很容易形成嵌套锁定, 进而导致死锁。 如果让主线程、副线程按相同的顺序加锁,就可以避免死锁问题。
  ➢ 使用定时锁:程序调用Lock对象的tryLock()方法加锁时可指定time和unit参数, 当超过指定时间后会自动释放对Lock的锁 定,这样就可以解开死锁了。
  ➢ 死锁检测:这是一种依靠算法来实现的死锁预防机制,它主要针对那些不可能实现按序加锁,也不能使用定时锁的场景。

6、线程通信

  当线程在系统内运行时, 线程的调度具有一定的透明性, 程序通常无法准确控制线程的轮换执行, 但Java也提供了一些机制来保证线 程协调运行。

6.1、传统的线程通信

  假设现在系统中有两个线程, 这两个线程分别代表存款者和取钱 者——现在假设系统有一种特殊的要求, 系统要求存款者和取钱者不 断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户 后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允 许取钱者连续两次取钱。
  为 了 实 现 这 种 功 能 , 可 以 借 助 于 Object 类 提 供 的 wait() 、 notify()和notifyAll()三个方法,这三个方法并不属于Thread类,而 是属于Object类。 但这三个方法必须由同步监视器对象来调用, 这可分成以下两种情况。
  ➢ 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器, 所以可以在同步方法中直接调用这三个方法。
  ➢ 对于使用synchronized修饰的同步代码块, 同步监视器是 synchronized后括号里的对象, 所以必须使用该对象调用这三 个方法。
  关于这三个方法的解释如下。
  ➢ wait():导致当前线程等待,直到其他线程调用该同步监视器 的notify()方法或notifyAll()方法来唤醒该线程。该wait()方 法有三种形式——无时间参数的wait(一直等待, 直到其他线 程通知)、带毫秒参数的wait()和带毫秒、毫微秒参数的 wait()(这两种方法都是等待指定时间后自动苏醒)。 调用 wait()方法的当前线程会释放对该同步监视器的锁定。
  ➢ notify():唤醒在此同步监视器上等待的单个线程。如果所有 线程都在此同步监视器上等待, 则会选择唤醒其中一个线程。 选择是任意性的。 只有当前线程放弃对该同步监视器的锁定后 (使用wait()方法),才可以执行被唤醒的线程。 ➢ notifyAll():唤醒在此同步监视器上等待的所有线程。 只有 当前线程放弃对该同步监视器的锁定后, 才可以执行被唤醒的线程。
  ➢ notifyAll():唤醒在此同步监视器上等待的所有线程。 只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

  程序中可以通过一个旗标来标识账户中是否已有存款, 当旗标为 false时,表明账户中没有存款,存款者线程可以向下执行,当存款者 把钱存入账户后,将旗标设为true,并调用notify()或notifyAll()方 法来唤醒其他线程;当存款者线程进入线程体后, 如果旗标为true就 调用wait()方法让该线程等待。
  当旗标为true时, 表明账户中已经存入了存款, 则取钱者线程可 以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调 用notify()或notifyAll()方法来唤醒其他线程;当取钱者线程进入线 程体后,如果旗标为false就调用wait()方法让该线程等待。
  本程序为Account类提供draw()和deposit()两个方法, 分别对应 该账户的取钱、存款等操作, 因为这两个方法可能需要并发修改 Account 类 的 balance 成 员 变 量 的 值 , 所 以 这 两 个 方 法 都 使 用 synchronized修饰成同步方法。 除此之外, 这两个方法还使用了wait()、notifyAll()来控制线程的协作。程序如下。

public class Account {
    // 封装账户编号、账户余额的两个成员变量
    private String accountNo;
    private double balance;
    // 标识账户中是否已有存款的旗标
    private boolean flag = false;

    public Account() {
    }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountNo() {
        return this.accountNo;
    }

    // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
    public double getBalance() {
        return this.balance;
    }

    public synchronized void draw(double drawAmount) {
        try {
            // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {
                wait();
            } else {
                // 执行取钱
                System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                // 将标识账户是否已有存款的旗标设为false。
                flag = false;
                // 唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public synchronized void deposit(double depositAmount) {
        try {
            // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
            if (flag) {// ①
                wait();
            } else {
                // 执行存款
                System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                // 将表示账户是否已有存款的旗标设为true
                flag = true;
                // 唤醒其他线程
                notifyAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

  上面程序使用wait()和notifyAll()进行了控制, 对存款者线程而言,当程序进入deposit()方法后,如果flag为true, 则表明账户中已有存款, 程序调用wait()方法阻塞;否则程序向下执 行存款操作, 当存款操作执行完成后, 系统将flag设为true, 然后调 用notifyAll()来唤醒其他被阻塞的线程——如果系统中有存款者线 程,存款者线程也会被唤醒,但该存款者线程执行到①号代码处时再 次进入阻塞状态, 只有执行draw()方法的取钱者线程才可以向下执 行。同理,取钱者线程的运行流程也是如此。 程序中的存款者线程循环100次重复存款, 而取钱者线程则循环 100次重复取钱, 存款者线程和取钱者线程分别调用Account对象的 deposit()、draw()方法来实现,取款代码如下。

public class DrawThread extends Thread {
    // 模拟用户账户
    private Account account;
    // 当前取钱线程所希望取的钱数
    private double drawAmount;

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    // 重复100次执行取钱操作
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.draw(drawAmount);
        }
    }
}

  存款代码如下。

public class DepositThread extends Thread {
    // 模拟用户账户
    private Account account;
    // 当前取钱线程所希望存款的钱数
    private double depositAmount;

    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }

    // 重复100次执行存款操作
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.deposit(depositAmount);
        }
    }
}

  主程序可以启动任意多个存款线程和取钱线程, 可以看到所有的取钱线程必须等存款线程存钱后才可以向下执行,而存款线程也必须 等取钱线程取钱后才可以向下执行。主程序测试代码如下。

public class DrawTest {
    public static void main(String[] args) {
        // 创建一个账户
        Account acct = new Account("1234567", 0);
        new DrawThread("取钱者", acct, 800).start();
        new DepositThread("存款者甲", acct, 800).start();
        new DepositThread("存款者乙", acct, 800).start();
        new DepositThread("存款者丙", acct, 800).start();
    }
}

  运行该程序,可以看到存款者线程、取钱者线程交替执行的情形,每当存款者向账户中存入800元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是800元,取钱结束后账户余额总是0元。运行该程序,会看到如下图所示的结果。
在这里插入图片描述

  显示程序最后被阻塞无法继续向下执行, 这是因为3个存 款者线程共有300次尝试存款操作,但1个取钱者线程只有100次尝试取钱操作,所以程序最后被阻塞,并不是因为死锁!

6.2、使用Condition控制线程通信

  如果程序不使用synchronized关键字来保证同步, 而是直接使用 Lock对象来保证同步, 则系统中不存在隐式的同步监视器, 也就不能 使用wait()、notify()、notifyAll()方法进行线程通信了。
  当使用Lock对象来保证同步时,Java提供了一个Condition类来保 持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行 的线程释放Lock对象, Condition对象也可以唤醒其他处于等待的线 程。
  Condition 将 同 步 监 视 器 方 法 ( wait() 、 notify() 和 notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对 象组合使用, 为每个对象提供多个等待集(wait-set)。 在这种情况 下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器 的功能。
  Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的 Condition 实 例 , 调 用 Lock 对 象 的 newCondition() 方 法 即 可 。 Condition类提供了如下三个方法。
  ➢ await():类似于隐式同步监视器上的wait()方法, 导致当前 线程等待, 直到其他线程调用该Condition的signal()方法或 signalAll()方法来唤醒该线程。 该await()方法有更多变体, 如 long awaitNanos ( long nanosTimeout ) 、 void awaitUninterruptibly()、awaitUntil(Date deadline)等, 可以完成更丰富的等待操作。
  ➢ signal():唤醒在此Lock对象上等待的单个线程。如果所有线 程都在该Lock对象上等待, 则会选择唤醒其中一个线程。 选择是任意性的。 只有当前线程放弃对该Lock对象的锁定后(使用 await()方法),才可以执行被唤醒的线程。
  ➢ signalAll():唤醒在此Lock对象上等待的所有线程。 只有当前线程放弃对该Lock对象的锁定后, 才可以执行被唤醒的线程。
  下面程序中Account使用Lock对象来控制同步, 并使用Condition 对象来控制线程的协调运行。

public class Account {
    // 显式定义Lock对象
    private final Lock lock = new ReentrantLock();
    // 获得指定Lock对象对应的Condition
    private final Condition cond = lock.newCondition();
    // 封装账户编号、账户余额的两个成员变量
    private String accountNo;
    private double balance;
    // 标识账户中是否已有存款的旗标
    private boolean flag = false;

    public Account() {
    }

    // 构造器
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountNo() {
        return this.accountNo;
    }

    // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
    public double getBalance() {
        return this.balance;
    }

    public void draw(double drawAmount) {
        // 加锁
        lock.lock();
        try {
            // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag) {
                cond.await();
            } else {
                // 执行取钱
                System.out.println(Thread.currentThread().getName()
                        + " 取钱:" + drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                // 将标识账户是否已有存款的旗标设为false。
                flag = false;
                // 唤醒其他线程
                cond.signalAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        // 使用finally块来释放锁
        finally {
            lock.unlock();
        }
    }

    public void deposit(double depositAmount) {
        lock.lock();
        try {
            // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
            if (flag)             // ①
            {
                cond.await();
            } else {
                // 执行存款
                System.out.println(Thread.currentThread().getName()
                        + " 存款:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                // 将表示账户是否已有存款的旗标设为true
                flag = true;
                // 唤醒其他线程
                cond.signalAll();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        // 使用finally块来释放锁
        finally {
            lock.unlock();
        }
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Account.class) {
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

  修改后运行测试主程序,效果还是和wait、notify一致。

6.3、使用阻塞队列(BlockingQueue)控制线程通信

  Java 5提供了一个BlockingQueue接口, 虽然BlockingQueue也是 Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步 的 工 具 。 BlockingQueue 具 有 一 个 特 征 : 当 生 产 者 线 程 试 图 向 BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当 消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则 该线程被阻塞。
  程序的两个线程通过交替向BlockingQueue中放入元素、取出元 素,即可很好地控制线程的通信。
  BlockingQueue提供如下两个支持阻塞的方法。
  ➢ put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
  ➢ take():尝试从BlockingQueue的头部取出元素, 如果该队列的元素已空,则阻塞该线程。

  BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方 法。这些方法归纳起来可分为如下三组。
  ➢ 在队列尾部插入元素。包括add(E e)、offer(E e)和put(E e) 方法, 当该队列已满时, 这三个方法分别会抛出异常、返回 false、阻塞队列。
  ➢ 在队列头部删除并返回删除的元素。包括remove()、poll()和 take()方法。 当该队列已空时, 这三个方法分别会抛出异常、 返回false、阻塞队列。
  ➢ 在队列头部取出但不删除元素。 包括element()和peek()方 法,当队列已空时,这两个方法分别抛出异常、返回false。
  BlockingQueue包含的方法之间的对应关系如下图所示。
在这里插入图片描述

  BlockingQueue与其实现类之间的类图如下图所示。
在这里插入图片描述
  上图中以黑色方框框出的都是Java 7新增的阻塞队列。 从上图可以看到,BlockingQueue包含如下5个实现类。
  ➢ ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
  ➢ LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  ➢ PriorityBlockingQueue:它并不是标准的阻塞队列。 与前面介绍的PriorityQueue类似, 该队列调用remove()、poll()、 take()等方法取出元素时, 并不是取出队列中存在时间最长的 元素,而是队列中最小的元素。PriorityBlockingQueue判断元 素的大小即可根据元素(实现Comparable接口)的本身大小来 自然排序,也可使用Comparator进行定制排序。
  ➢ SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
  ➢ DelayQueue : 它 是 一 个 特 殊 的 BlockingQueue , 底 层 基 于 PriorityBlockingQueue实现。不过,DelayQueue要求集合元素 都 实 现 Delay 接 口 ( 该 接 口 里 只 有 一 个 long getDelay() 方 法), DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

  下面以ArrayBlockingQueue为例介绍阻塞队列的功能和用法。 下面先用一个最简单的程序来测试BlockingQueue的put()方法。

public class BlockingQueueTest {
    public static void main(String[] args) throws Exception {
        // 定义一个长度为2的阻塞队列
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
        bq.put("Java"); // 与bq.add("Java")、bq.offer("Java")相同
        bq.put("Java"); // 与bq.add("Java")、bq.offer("Java")相同
        bq.put("Java"); // ① 阻塞线程。
    }
}

  上面程序先定义一个大小为2的BlockingQueue, 程序先向该队列 中放入两个元素,此时队列还没有满,两个元素都可以放入,因此使 用put()、add()和offer()方法效果完全一样。当程序试图放入第三个 元素时,如果使用put()方法尝试放入元素将会阻塞线程,如上面程序 ①号代码所示。如果使用add()方法尝试放入元素将会引发异常;如果 使用offer()方法尝试放入元素则会返回false,元素不会被放入。
  与此类似的是,在BlockingQueue已空的情况下,程序使用take() 方法尝试取出元素将会阻塞线程;使用remove()方法尝试取出元素将引发异常;使用poll()方法尝试取出元素将返回false,元素不会被删除。
  掌握了BlockingQueue阻塞队列的特性之后,下面程序就可以利用 BlockingQueue来实现线程通信了。

class Producer extends Thread {
    private BlockingQueue<String> bq;

    public Producer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    public void run() {
        String[] strArr = new String[]{"Java", "Struts", "Spring"};
        for (int i = 0; i < 999999999; i++) {
            System.out.println(getName() + "生产者准备生产集合元素!");
            try {
                Thread.sleep(200);
                // 尝试放入元素,如果队列已满,线程被阻塞
                bq.put(strArr[i % 3]);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            System.out.println(getName() + "生产完成:" + bq);
        }
    }
}

class Consumer extends Thread {
    private BlockingQueue<String> bq;

    public Consumer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    public void run() {
        while (true) {
            System.out.println(getName() + "消费者准备消费集合元素!");
            try {
                Thread.sleep(200);
                // 尝试取出元素,如果队列已空,线程被阻塞
                bq.take();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}

public class BlockingQueueTest2 {
    public static void main(String[] args) {
        // 创建一个容量为1的BlockingQueue
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        // 启动3条生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        // 启动一条消费者线程
        new Consumer(bq).start();
    }
}

  上面程序启动了3个生产者线程向BlockingQueue集合放入元素, 启动了1个消费者线程从BlockingQueue集合取出元素。 本程序的 BlockingQueue集合容量为1,因此3个生产者线程无法连续放入元素, 必须等待消费者线程取出一个元素后,3个生产者线程的其中之一才能放入一个元素。运行该程序,会看到如下图所示的结果。
在这里插入图片描述
  从上图可以看出,3个生产者线程都想向BlockingQueue中放入元素,但只要其中一个线程向该队列中放入元素之后,其他生产者线 程就必须等待,等待消费者线程取出BlockingQueue队列里的元素。

7、线程池

  系统启动一个新线程的成本是比较高的, 因为它涉及与操作系统 交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
  与数据库连接池类似的是, 线程池在系统启动时即创建大量空闲的线程, 程序将一个Runnable对象或Callable对象传给线程池, 线程 池就会启动一个空闲的线程来执行它们的run()或call()方法, 当 run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线 程池中成为空闲状态,等待执行下一个Runnable对象的run()或call() 方法。
  除此之外, 使用线程池可以有效地控制系统中并发线程的数量, 当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。

7.1、使用线程池管理线程

  在Java 5以前, 开发者必须手动实现自己的线程池;从Java 5开始,Java内建支持线程池。Java 5新增了一个Executors工厂类来产生 线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
  ➢ newCachedThreadPool():创建一个具有缓存功能的线程池, 系统根据需要创建线程,这些线程将会被缓存在线程池中。
  ➢ newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
  ➢ newSingleThreadExecutor():创建一个只有单线程的线程池, 它相当于调用newFixedThread Pool()方法时传入参数为 1。
  ➢ newScheduledThreadPool(int corePoolSize):创建具有指定 线程数的线程池,它可以在指定延迟后执行线程任务。 corePoolSize指池中所保存的线程数, 即使线程是空闲的也被 保存在线程池内。
  ➢ newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
  ➢ ExecutorService newWorkStealingPool(int parallelism): 创建持有足够的线程的线程池来支持给定的并行级别, 该方法还会使用多个队列来减少竞争。
  ➢ ExecutorService newWorkStealingPool():该方法是前一个方法的简化版本。 如果当前机器有4个CPU, 则目标并行级别被 设置为4,也就是相当于为前一个方法传入4作为参数。

  上面7个方法中的前三个方法返回一个ExecutorService对象, 该 对象代表一个线程池, 它可以执行Runnable对象或Callable对象所代 表的线程;而中间两个方法返回一个ScheduledExecutorService线程 池, 它是ExecutorService的子类, 它可以在指定延迟后执行线程任 务;最后两个方法则是Java 8新增的,这两个方法可充分利用多CPU并 行的能力。 这两个方法生成的work stealing池, 都相当于后台线程 池,如果所有的前台线程都死亡了,work stealing池中的线程会自动死亡。
  ExecutorService代表尽快执行线程的线程池(只要线程池中有空 闲线程, 就立即执行线程任务), 程序只要将一个Runnable对象或 Callable对象(代表线程任务)提交给该线程池, 该线程池就会尽快 执行该任务。
  ExecutorService里提供了如下三个方法。
    ➢ Future<?> submit(Runnable task):将一个Runnable对象提 交给指定的线程池, 线程池将在有空闲线程时执行Runnable对 象代表的任务。 其中Future对象代表Runnable任务的返回值 ——但run()方法没有返回值, 所以Future对象将在run()方法 执 行 结 束 后 返 回 null 。 但 可 以 调 用 Future 的 isDone() 、 isCancelled()方法来获得Runnable对象的执行状态。
    ➢ Futuresubmit(Runnable task, T result) : 将 一 个 Runnable对象提交给指定的线程池, 线程池将在有空闲线程时 执行Runnable对象代表的任务。 其中result显式指定线程执行 结束后的返回值,所以Future对象将在run()方法执行结束后返 回result。
    ➢ Futuresubmit(Callabletask):将一个Callable对 象提交给指定的线程池,线程池将在有空闲线程时执行 Callable对象代表的任务。 其中Future代表Callable对象里 call()方法的返回值。

  ScheduledExecutorService代表可在指定延迟后或周期性地执行 线程任务的线程池,它提供了如下4个方法。
    ➢ ScheduledFutureschedule(Callablecallable, long delay, TimeUnit unit):指定callable任务将在delay延迟后 执行。
    ➢ ScheduledFuture<?>schedule(Runnable command, long delay, TimeUnit unit):指定command任务将在delay延迟后执 行。
    ➢ ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定频 率重复执行。 也就是说, 在initialDelay后开始执行, 依次在 initialDelay+period 、 initialDelay+2*period… 处 重 复 执 行,依此类推。
    ➢ ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操 作, 随后在每一次执行终止和下一次执行开始之间都存在给定 的延迟。 如果任务在任一次执行时遇到异常, 就会取消后续执 行;否则,只能通过程序来显式取消或终止该任务。

  用完一个线程池后, 应该调用该线程池的shutdown()方法, 该方 法将启动线程池的关闭序列, 调用shutdown()方法后的线程池不再接 收新任务,但会将以前所有已提交任务执行完成。当线程池中的所有 任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程 池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行 的活动任务, 暂停处理正在等待的任务, 并返回等待执行的任务列表。
  使用线程池来执行线程任务的步骤如下。
  ① 调用Executors类的静态工厂方法创建一个ExecutorService对 象,该对象代表一个线程池。
  ② 创建Runnable实现类或Callable实现类的实例,作为线程执行 任务。
  ③ 调用ExecutorService对象的submit()方法来提交Runnable实 例或Callable实例。
  ④ 当 不 想 提 交 任 何 任 务 时 , 调 用 ExecutorService 对 象 的 shutdown()方法来关闭线程池。
  下面程序使用线程池来执行指定Runnable对象所代表的任务。

public class ThreadPoolTest {
    public static void main(String[] args)
            throws Exception {
        // 创建足够的线程来支持4个CPU并行的线程池
        // 创建一个具有固定线程数(6)的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 使用Lambda表达式创建Runnable对象
        Runnable target = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的i值为:" + i);
            }
        };
        // 向线程池中提交两个线程
        pool.submit(target);
        pool.submit(target);
        // 关闭线程池
        pool.shutdown();
    }
}

  上面程序中创建Runnable实现类与最开始创建线程池并没有太大 差别, 创建了Runnable实现类之后程序没有直接创建线程、启动线程 来执行该Runnable任务, 而是通过线程池来执行该任务,运行上面程序,将看到两个线程交替执行的效果,如下图所示。
在这里插入图片描述

7.2、使用ForkJoinPool利用多CPU

  现在计算机大多已向多CPU方向发展,即使普通PC,甚至小型智能 设备(如手机)、多核处理器也已被广泛应用。在未来的日子里,处 理器的核心数将会发展到更多。
  虽然硬件上的多核CPU已经十分成熟,但很多应用程序并未为这种 多核CPU做好准备,因此并不能很好地利用多核CPU的性能优势。
  为了充分利用多CPU、多核CPU的性能优势, 计算机软件系统应该 可以充分“挖掘”每个CPU的计算能力, 绝不能让某个CPU处于“空 闲”状态。 为了充分利用多CPU、多核CPU的优势, 可以考虑把一个任 务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上 并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并 起来即可。
  Java 7提供了ForkJoinPool来支持将一个任务拆分成多个“小任 务”并行计算, 再把多个“小任务”的结果合并成总的计算结果。 ForkJoinPool是ExecutorService的实现类, 因此是一种特殊的线程 池。ForkJoinPool提供了如下两个常用的构造器。
  ➢ ForkJoinPool(int parallelism):创建一个包含parallelism 个并行线程的ForkJoinPool。
  ➢ ForkJoinPool():以Runtime.availableProcessors()方法的 返回值作为parallelism参数来创建Fork JoinPool。

  Java 8 进 一 步 扩 展 了 ForkJoinPool 的 功 能 , Java 8 为 ForkJoinPool增加了通用池功能。 ForkJoinPool类通过如下两个静态方法提供通用池功能。
  ➢ ForkJoinPool commonPool():该方法返回一个通用池, 通用 池的运行状态不会受shutdown()或shutdownNow()方法的影响。 当然, 如果程序直接执行System.exit(0);来终止虚拟机, 通 用池以及通用池中正在执行的任务都会被自动终止。
  ➢ int getCommonPoolParallelism():该方法返回通用池的并行级别。

  创建了ForkJoinPool 实 例 之 后 , 就 可 调 用 ForkJoinPool 的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执 行指定任务了。 其中ForkJoinTask代表一个可以并行、合并的任务。
  ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction 和 RecursiveTask 。 其 中 RecursiveTask 代 表 有 返 回 值 的 任 务 , 而 RecursiveAction代表没有返回值的任务。下图显示了ForkJoinPool、ForkJoinTask等类的类图。
在这里插入图片描述
  下面以执行没有返回值的“大任务”(简单地打印0~300的数 值)为例,程序将一个“大任务”拆分成多个“小任务”,并将任务交给ForkJoinPool来执行。

// 继承RecursiveAction来实现"可分解"的任务
class PrintTask extends RecursiveAction {
    // 每个“小任务”只最多只打印50个数
    private static final int THRESHOLD = 50;
    private int start;
    private int end;

    // 打印从start到end的任务
    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        // 当end与start之间的差小于THRESHOLD时,开始打印
        if (end - start < THRESHOLD) {
            for (int i = start; i < end; i++) {
                System.out.println(Thread.currentThread().getName()
                        + "的i值:" + i);
            }
        } else {
            // 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过50个
            // 将大任务分解成两个小任务。
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            // 并行执行两个“小任务”
            left.fork();
            right.fork();
        }
    }
}

public class ForkJoinPoolTest {
    public static void main(String[] args)
            throws Exception {
        ForkJoinPool pool = new ForkJoinPool();
        // 提交可分解的PrintTask任务
        pool.submit(new PrintTask(0, 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        // 关闭线程池
        pool.shutdown();
    }
}

  运行效果如下图。
在这里插入图片描述
  从上图所示的执行结果来看, ForkJoinPool启动了4个线程 来执行这个打印任务——这是因为测试计算机的CPU是4核的。 不仅如 此,读者可以看到程序虽然打印了0~299这300个数字,但并不是连续 打印的,这是因为程序将这个打印任务进行了分解,分解后的任务会 并行执行,所以不会按顺序从0打印到299。
  上面定义的任务是一个没有返回值的打印任务, 如果大任务是有返回值的任务,则可以让任务继承RecursiveTask,其中泛型参数T就代表了该任务的返回值类型。 下面程序示范了使用Recursive Task 对一个长度为100的数组的元素值进行累加。

// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer> {
    // 每个“小任务”只最多只累加20个数
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;

    // 累加从start到end的数组元素
    public CalTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 当end与start之间的差小于THRESHOLD时,开始进行实际累加
        if (end - start < THRESHOLD) {
            for (var i = start; i < end; i++) {
                sum += arr[i];
            }
            return sum;
        } else {
            // 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
            // 将大任务分解成两个小任务。
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr, start, middle);
            CalTask right = new CalTask(arr, middle, end);
            // 并行执行两个“小任务”
            left.fork();
            right.fork();
            // 把两个“小任务”累加的结果合并起来
            return left.join() + right.join();    // ①
        }
    }
}

public class Sum {
    public static void main(String[] args)
            throws Exception {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        // 初始化100个数字元素
        for (int i = 0, len = arr.length; i < len; i++) {
            int tmp = rand.nextInt(20);
            // 对数组元素赋值,并将数组元素的值添加到sum总和中。
            total += (arr[i] = tmp);
        }
        System.out.println("初始化数组时,所有元素之和为:" + total);
        // 创建一个通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        // 提交可分解的CalTask任务
        Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
        System.out.println("ForkJoin计算出的元素之和为:" + future.get());
        // 关闭线程池
        pool.shutdown();
    }
}

  运行上面程序,将可以看到程序通过CalTask计算出来的总和,与 初始化数组元素时统计出来的总和总是相等,这表明程序一切正常。

8、线程相关类

8.1、ThreadLocal类

  ThreadLocal, 是Thread Local Variable(线程局部变量)的意 思 , 也 许 将 它 命 名 为 ThreadLocalVar 更 加 合 适 。 线 程 局 部 变 量 (ThreadLocal)的功用其实非常简单,就是为每一个使用该变量的线 程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的 副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一 个线程都完全拥有该变量一样。
  ThreadLocal类的用法非常简单, 它只提供了如下三个public方法。
  ➢ T get():返回此线程局部变量中当前线程副本中的值。
  ➢ void remove():删除此线程局部变量中当前线程的值。
  ➢ void set(T value):设置此线程局部变量中当前线程副本中
的值。
  下面程序将向读者证明ThreadLocal的作用。

class Account {
    /* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
    每个线程都会保留该变量的一个副本 */
    private ThreadLocal<String> name = new ThreadLocal<>();

    // 定义一个初始化name成员变量的构造器
    public Account(String str) {
        this.name.set(str);
        // 下面代码用于访问当前线程的name副本的值
        System.out.println("---" + this.name.get());
    }

    // name的setter和getter方法
    public String getName() {
        return name.get();
    }

    public void setName(String str) {
        this.name.set(str);
    }
}


class MyTest extends Thread {
    // 定义一个Account类型的成员变量
    private Account account;

    public MyTest(Account account, String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        // 循环10次
        for (int i = 0; i < 10; i++) {
            // 当i == 6时输出将账户名替换成当前线程名
            if (i == 6) {
                account.setName(getName());
            }
            // 输出同一个账户的账户名和循环变量
            System.out.println(account.getName() + " 账户的i值:" + i);
        }
    }
}

public class ThreadLocalTest {
    public static void main(String[] args) {
        // 启动两条线程,两条线程共享同一个Account
        Account at = new Account("初始名");
		/*
		虽然两条线程共享同一个账户,即只有一个账户名
		但由于账户名是ThreadLocal类型的,所以每条线程
		都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
		线程访问同一个账户时看到不同的账户名。
		*/
        new MyTest(at, "线程甲").start();
        new MyTest(at, "线程乙").start();
    }
}

  运行结果如下图。
在这里插入图片描述
  从上面程序可以看出, 实际上账户名有三个副本, 主线程一个, 另外启动的两个线程各一个,它们的值互不干扰,每个线程完全拥有 自己的ThreadLocal变量,这就是ThreadLocal的用途。
  ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中 对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实 现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所 以要使用这种同步机制,需要很细致地分析在什么时候对变量进行读 写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这 种情况下,系统并没有将这份资源复制多份,只是采用了安全机制来 控制对这份资源的访问而已。
  ThreadLocal 从 另 一 个 角 度 来 解 决 多 线 程 的 并 发 访 问 , ThreadLocal将需要并发访问的资源复制多份, 每个线程拥有一份资 源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进 行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代 码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与 线程相关的状态使用ThreadLocal保存。
  ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共 享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。
  通常建议:如果多个线程之间需要共享资源, 以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享 冲突,则可以使用ThreadLocal。

8.2、包装线程不安全的集合

  Java集合中的ArrayList、LinkedList、HashSet、 TreeSet、HashMap、TreeMap等都是线程不安全的,也就是说,当多个 并发线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性。
  如果程序中有多个线程可能访问以上这些集合, 就可以使用 Collections 提 供 的 类 方 法 把 这 些 集 合 包 装 成 线 程 安 全 的 集 合 。 Collections提供了如下几个静态方法。
  ➢ Collection synchronizedCollection(Collection c):返回指定collection对应的线程安全的collection。
  ➢ static List synchronizedList(List list):返 回指定List对象对应的线程安全的List对象。
  ➢ static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返 回指定Map对象对应的线程安全的Map对象。
  ➢ static Set synchronizedSet(Set s):返回指定 Set对象对应的线程安全的Set对象。
  ➢ static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) : 返 回 指 定 SortedMap对象对应的线程安全的SortedMap对象。 static SortedSet synchronizedSortedSet(SortedSet s) : 返 回 指 定 SortedSet对象对应的线程安全的SortedSet对象。
  例如需要在多线程中使用线程安全的HashMap对象,则可以采用如下代码:

        //使用 Collections的synchronizedMap方法将一个普通的map包装成线程安全的类
        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

  如果需要把某个集合包装成线程安全的集合,则应该在创建之 后立即包装,如上面程序所示——当HashMap对象创建后立即被包装成线程安全的HashMap对象。

8.3、线程安全的集合类

  从Java 5开始,在java.util.concurrent包下提供了大量 支持高效并发访问的集合接口和实现类,如下图所示。
在这里插入图片描述
  从上图所示的类图可以看出,这些线程安全的集合类可分为如下两类。

  ➢ 以 Concurrent 开 头 的 集 合 类 , 如 ConcurrentHashMap 、ConcurrentSkipListMap 、 ConcurrentSkip ListSet 、ConcurrentLinkedQueue和ConcurrentLinkedDeque。
  ➢ 以 CopyOnWrite 开 头 的 集 合 类 , 如 CopyOnWriteArrayList 、
CopyOnWriteArraySet。

  其中以Concurrent开头的集合类代表了支持并发访问的集合, 它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的, 但读取操作不必锁定。 以Concurrent开头的集合类采用了 更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。以 CopyOnWrite开头的正如名称一样,它采 用复制底层数组的方式来实现写操作,集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值