Java核心技术1-并发

读《Java核心技术1》记录知识点

并发

什么是线程

  1. 多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常,每一个任务称为一个线程( thread), 它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。
  2. 多进程与多线程有哪些区别呢? 本质的区别在于每个进程拥有自己的一整套变量, 而线程则共享数据。 这听起来似乎有些风险。然而,共享变量使线程之间的通信比进程之间的通信更有效、 更容易。 此外, 在有些操作系统中, 与进程相比较, 线程更“ 轻量级”, 创建、 撤销一个线程比启动新进程的开销要小得多。
  3. 调用 Threadsleep 不会创建一个新线程,sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。
  4. Runnable 接口只有一个 run 方法

    中断线程

  5. 当线程的 run 方法执行方法体中最后一条语句后, 并经由执行 return 语句返冋时, 或者出现了在方法中没有捕获的异常时,线程将终止。 在 Java 的早期版本中, 还有一个 stop方法, 其他线程可以调用它终止线程。但是, 这个方法现在已经被弃用了。

  6. 没有可以强制线程终止的方法。然而, interrupt 方法可以用来请求终止线程。
  7. 当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每一个线程都具有的 boolean 标志。每个线程都应该不时地检査这个标志, 以判断线程是否被中断。
    要想弄清中断状态是否被置位,首先调用静态的 Thread.currentThread 方法获得当前线程, 然后调用 islnterrupted 方法
  8. 如果线程被阻塞, 就无法检测中断状态。这是产生 InterruptedExceptioii 异常的地方。当在一个被阻塞的线程(调用 sleep 或 wait ) 上调用 interrupt 方法时, 阻塞调用将会被Interrupted Exception 异常中断。

线程状态

  1. 线程可以有如下 6 种状态:

    • New (新创建):当用 new 操作符创建一个新线程时, 如 newThread(r), 该线程还没有开始运行。这意味着它的状态是 new。当一个线程处于新创建状态时, 程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。
    • Runnable (可运行):一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线桿可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。

    一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。 当时间片用完,操作系统剥夺该线程的运行权, 并给另一个线程运行机会。当选择下一个线程时, 操作系统考虑线程的优先级 。
    现在所有的桌面以及服务器操作系统都使用抢占式调度。但是,像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用 yield 方法、 或者被阻塞或等待时,线程才失去控制权。
    在具有多个处理器的机器上,每一个处理器运行一个线程, 可以有多个线程并行运行。当然, 如果线程的数目多于处理器的数目, 调度器依然采用时间片机制。
    记住,在任何给定时刻,二个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行。)

    • Blocked (被阻塞):当一个线程试图获取一个内部的对象锁(而不是 javiutiUoncurrent 库中的锁,) 而该锁被其他线程持有, 则该线程进人阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
    • Waiting (等待):当线程等待另一个线程通知调度器一个条件时, 它自己进入等待状态。 在调用 Object.wait 方法或 Thread.join 方法, 或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时, 就会出现这种情况。
    • Timed waiting (计时等待):有几个方法有一个超时参数。调用它们导致线程进人计时等待(timed waiting) 状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有:
      Thread.sleep 和 Object.wait、 Thread.join、 Lock.tryLock 以及 Condition.await 的计时版。
    • Terminated (被终止)线程因如下两个原因之一而被终止:
    1. 因为 run 方法正常退出而自然死亡。
    2. 因为一个没有捕获的异常终止了 run 方法而意外死亡。

线程属性

  1. 线程的各种属性,其中包括:
  • 线程优先级:在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下, 一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 ) 与 MAX_PRIORITY (定义为 10 ) 之间的任何值。NORM_PRIORITY 被定义为 5。
    每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优先级被映射到宿主机平台的优先级上, 优先级个数也许更多,也许更少。
    例如, Windows 有 7 个优先级别。一些 Java 优先级将映射到相同的操作系统优先级。在Oracle 为 Linux 提供的 Java 虚拟机中, 线程的优先级被忽略一所有线程具有相同的优先级。
  • 守护线程:可以通过调用t.setDaemon(true);将线程转换为守护线程(daemon thread)。这样一个线程没有什么神奇。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“ 计时器嘀嗒” 信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时, 虚拟机就退出了,由于如果只剩下守护线程, 就没必要继续运行程序了。
  • 线程组以及处理未捕获异常的处理器:线程的 run方法不能抛出任何受查异常, 但是,非受査异常会导致线程终止。在这种情况下,线程就死亡了。但是,不需要任何 catch 子句来处理可以被传播的异常。相反, 就在线程死亡之前, 异常被传递到一个用于未捕获异常的处理器。

同步

  1. 有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。

  2. 用 ReentrantLock 保护代码块的基本结构如下:
    myLock.lock(); // a ReentrantLock object

    1
    2
    3
    4
    5
    6
    7
    8
    try
    {
    critical section
    }
    finally
    {
    myLock.unlock();// make sure the lock is unlocked even if an exception is thrown
    }
  3. 把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,
    锁必须被释放。 否则, 其他线程将永远阻塞。

  4. 有关锁和条件的关键之处:

  • 锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 锁可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
  1. 锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器(monitor)

  2. 监视器具有如下特性:

    • 监视器是只包含私有域的类。
    • 每个监视器类的对象有一个相关的锁。
    • 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj.method(), 那 么 obj对象的锁是在方法调用开始时自动获得, 并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问该域。
    • 该锁可以有任意多个相关条件。
  3. volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

  4. 线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。 tryLock 方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回false, 而且线程可以立即离开去做其他事情。

  5. lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁, 那么, lock 方法就无法终止。
    然而, 如果调用带有用超时参数的 tryLock, 那么如果线程在等待期间被中断,将抛出InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。
    也可以调用 locklnterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。

  6. java.util.concurrent.locks 包 定 义 了 两 个 锁 类, 我 们 已 经 讨 论 的 ReentrantLock 类 和
    ReentrantReadWriteLock 类。 如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话, 后者是十分有用的。在这种情况下, 允许对读者线程共享访问是合适的。当然, 写者线程依然必须是互斥访问的。

  7. 为什么弃用 stop 和 suspend 方法?

    初始的 Java 版本定义了一个 stop 方法用来终止一个线程, 以及一个 suspend 方法用来阻塞一个线程直至另一个线程调用 resume。stop 和 suspend 方法有一些共同点:都试图控制一个给定线程的行为。
    stop、 suspend 和 resume 方法已经弃用。stop 方法天生就不安全,经验证明 suspend 方法会经常导致死锁。
    首先来看看 stop 方法, 该方法终止所有未结束的方法, 包括 run 方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如’假定 TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。
    当线程要终止另一个线程时, 无法知道什么时候调用 stop 方法是安全的, 什么时候导致
    对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程, 被中断的线程
    会在安全的时候停止。

    与 stop 不同,suspend 不会破坏对象。但是,如果用 suspend 挂起一个持有一个锁的线程, 那么,该锁在恢复之前是不可用的。 如果调用suspend 方法的线程试图获得同一个锁, 那么程序死锁: 被挂起的线程等着被恢复, 而将其挂起的线程等待获得锁。

阻塞队列

  1. 对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插人元素, 消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。 例如, 考虑银行转账程序, 转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。 另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。因此不需要同步。(当然, 线程安全的队列类的实现者不能不考虑锁和条件,但是, 那是他们的问题而不是你的问题。)

  2. 当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队列(blocking queue ) 导致线程阻塞。在协调多个线程之间的合作时, 阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢, 第二个线程集在等待结果时会阻塞。 如果第一个线程集运行得快, 它将等待第二个队列集赶上来。

  3. java.util.concurrent 包提供了阻塞队列的几个变种。 默认情况下, LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数, 则那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。

    PriorityBlockingQueue 是一个带优先级的队列, 而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的, 取元素的操作会阻塞。

线程安全的集合

  1. java.util.concurrent 包提供了映射、 有序集和队列的高效实现: ConcurrentHashMap、ConcurrentSkipListMap > ConcurrentSkipListSet 和 ConcurrentLinkedQueue。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
  2. CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合, 其中所有的修改线程对底层数组进行复制。 如果在集合上进行迭代的线程数超过修改线程数, 这样的安排是很有用的。当构建一个迭代器的时候, 它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组, 但是,集合的数组已经被替换了。因而,旧的迭代器拥有一致的(可能过时的)视图,访问它无须任何同步开销。

Callable与Future

  1. Runnable 封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable 与 Runnable 类似, 但是有返回值。Callable 接口是一个参数化的类型, 只有一个方法 call。
  2. Future 保存异步计算的结果。可以启动一个计算,将 Future 对象交给某个线程,然后忘掉它。 Future 对象的所有者在结果计算好之后就可以获得它。

执行器

  1. 构建一个新的线程是有一定代价的, 因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool )。一个线程池中包含许多准备运行的空闲线程。 将 Runnable 对象交给线程池, 就会有一个线程调用 run 方法。 当 run 方法退出时, 线程不会死亡,而是在池中准备为下一个请求提供服务。

    构建一个新的线程是有一定代价的, 因为涉及与操作系统的交互。如果程序中创建了大
    量的生命期很短的线程,应该使用线程池(thread pool )。一个线程池中包含许多准备运行的
    空闲线程。 将 Runnable 对象交给线程池, 就会有一个线程调用 run 方法。 当 run 方法退出
    时, 线程不会死亡,而是在池中准备为下一个请求提供服务。

  2. 执行器 ( Executor) 类有许多静态工厂方法用来构建线程池。