笔记:Java多线程

线程简述

进程是一个程序的一个运行中的实例,os为进程分配内存、IO等资源。进程拥有若干个(至少一个)线程来获得CPU执行其中的指令,线程使用进程的资源。

使用

线程的典型使用流程如下:

  1. 实现Runnable接口,具体的执行逻辑。
  2. 创建一个Thread对象,Runable作为它的任务。
  3. 调用start方法开启线程。
  4. 任务完成或发生异常后线程终止。

不推荐继承Thread类的方式,尤其是Thread有类似线程池那样的管理机制时。应该把线程的管理和任务的提交分别对待。

通常自己可以为Runable类设置标记字段来取消或者暂停线程的执行等,但线程机制有自己的一套功能实现这些。

线程的状态

可以调用getState方法获得当前线程对象的运行状态,包括:

  • NEW
    The thread has been created, but has never been started.

  • RUNNABLE
    可运行,或正在运行中。(启动后,或每次重新获得CPU时间时)。

  • WAITING
    线程(调用await)等待条件成立,之后被其它线程(调用signalAll)唤醒。

  • TIMED_WAITING
    计时等待:包括Thread.sleep,Thread.join,Object.wait,Lock.tryLock,Condition.await。

  • BLOCKED
    线程等待锁而阻塞。

  • TERMINATED
    run方法正常执行结束或发生异常而终止。

线程的生命周期

线程的生命周期表现为其几种状态之间的转换:

线程状态转换

使用线程是为了异步地执行任务,虽然可以限定线程的执行时间,但是通常地线程的执行周期是未知的。一个线程完成其run方法的正常退出后就结束了,此外,若run方法执行期间发生异常,那么线程也立即终止。

线程达到终止有下面的方式:

正常结束

  • 被动结束:
    线程执行完毕。

  • 主动方式:
    可以结合标记字段,从设计上让线程的代码退出。但这不是最好的,对于循环执行的任务,只能在当前循环之后判断标记然后是否继续运行。更有的线程任务,只能做到“不可打断的原子操作——例如一个读取db的操作,网络请求”执行后在结束时根据标记来放弃后续处理(一般也就是不执行回调)。从代码设计上终止线程很难达到立即的目标。

Thread类提供了stop方法来“立即,强制”终止线程,但这是不合理的:
在无法预知当前线程的逻辑执行的程度时终止线程会导致相关状态的不一致。

Thread提供了interrupt()和isInterrupted()来到达上述的“标记”字段的目标,而且更具优势。因为interrupt()可以让一些非运行状态下的线程(sleep、wait、await、tryLock等)抛出InterruptedException异常来让run方法获得有关“中断”的通知。
不过:如果执行了一些本身就阻塞的语句,如db读写和网络请求等,中断是无法立即被响应的。

异常结束

自身逻辑的执行抛出了异常,那么线程立即中断。可以为线程设置异常处理器。

线程的设置

未捕获异常处理

  • 由于run方法未声明任何已检查异常,所以Thread子类run方法中不能抛出任何已检查异常。当抛出未检查异常时,线程终止。

  • 可以使用setUncaughtExceptionHandler方法为线程指定一个异常处理器,做一些记录日志这样的处理。Thread.setDefaultUncaughtExceptionHandler静态方法可以为所有线程指定默认的异常处理器。

线程优先级

线程优先级决定了OS对线程调度的优先顺序。Thread类提供了MAX_PRIORITY = 10、MIN_PRIORITY = 1和NORM_PRIORITY = 5三个优先级常量。默认情况下线程自动获得启动它的线程的优先级,使用setPriority(int priority)方法来改变优先级,priority是介于MAX_PRIORITY和MIN_PRIORITY之间的整数。
具体的OS划分的线程优先级数量是不同的,例如android的api 21版本中,系统就提供了10个不同的优先级常量(在android.os.Process.THREAD_PRIORITY_xxx)。JVM在运行时会将设置的priority映射到对应OS提供的优先级上,有可能不同的priority值最终对应相同的优先级。所以不要依赖优先级的数值来保证线程运行的严格顺序。

守护线程

守护线程的目标是为其它线程服务,可以调用setDaemon(boolean isDaemon)方法在启动线程前设置它为守护线程。如果进程中只剩下守护线程,那么其运行状态和没有任何线程在进行中是一样的,系统随时会终止进程,也就是说守护线程在任何时候都可以被中断。因为它随时可能被终止——finally块的执行也无法保证,所以不要在其中获取需要释放的资源。

线程同步

被多个线程共同访问的变量“一般是”需要同步。线程内部的变量(方法局部变量)是无需同步的,因为各个线程自己有一份,而像成员变量需要考虑同步。

变量需要同步的原因:线程对变量的操作不是原子的。
下面的:

count++;

发生了三步操作:

  1. 读取count的值到线程私有的寄存器。
  2. count加1。
  3. 寄存器中的count写回原count。

更多的场景下,多个操作一起发生(语句块或整个方法体),涉及到若干个状态的读取和赋值,此时需要的同步已经不是简单的基本类型变量的访问了,而是保证操作集合的同步(有序)执行。
所以,同步的最终结果是状态的正确性,但是同步本身是针对语句(代码块)进行的。

Lock和Condition

锁用来保护代码块被同步访问。为需要同步处理的状态准备一个锁对象,之后将代码块放入锁对象的lock/unlock之间就可以让所有线程去竞争锁,每次只有一个线程获得锁并执行对应的代码块。

如果被同步的代码块的执行需要某个先决条件,可以使用锁对象创建一个关联的Condition对象来达到此目的(一个锁可以创建多个关联的条件对象)。

条件对象的await()方法使得当前线程转为阻塞状态,并立即释放锁。之后有其它线程调用该条件对象的signalAll()方法后(通知线程,但不保证条件一定成立)阻塞的线程再次转为可运行,再次获得锁后await()方法返回(Each thread must re-acquire the lock before it can return from await()),之后还需要继续判定条件。

为了实现异步任务等待条件成立的目标时,await-signalAll方式要比自己构造while-sleep这样的代码要好得多,因为sleep的间隔不好控制,消耗较大,而且不够及时。

锁和条件的的标准使用方式如下:

private boolean conditionSatisfied;
private ReentrantLock mLock = new ReentrantLock();
private Condition mCondition = mLock.newCondition();

private void syncMethod() throws InterruptedException {
    mLock.lock();
    try {
        while (!conditionSatisfied) {
            // await()返回后应该继续判断条件,进而继续等待或执行
            mCondition.await();
        }

        // ...
        // 需要同步的代码块
        // ...

        // 状态向着条件可能成立的方向发展了,通知其它线程
        mCondition.signalAll();
    } finally {
        mLock.unlock();
    }
}

注意:

  • 锁是可重进的,线程可以重复获得已获得锁,这样需要锁的方法的代码中可以继续执行需要该锁的其它方法。锁内部维护“获得/释放”的计数。

  • await()使得线程等待期间(waiting)可能发生InterruptedException异常,它和线程的stop有关,需要同步的方法一般声明抛出就行了。

  • java的Object类中有一些成员使得每个对象都有一个默认的锁,并关联一个默认的条件对象。为方法添加synchronized关键字正是使用了此内部锁完成同步的,相当于为方法体整个添加lock/unlock(等同synchronized(this))。很显然对象的所有同步方法都会一起被同步。静态方法使用类型对象的内部锁,所有静态同步方法一起被同步

同步方法或synchronized(this)中使用wait/notifyAll完成await/signalAll相同的工作。只是一种语法上的简化,在其它地方调用wait/notifyAll会引发IllegalMonitorStateException异常——因为没有获得关联的内部对象锁。

上面的代码可以简化为:

private synchronized void syncMethod() throws InterruptedException {
    while (!conditionSatisfied) {
        wait();
    }

    // ...
    // 是需要同步的代码块
    // ...

    notifyAll();
}

NOTE:相比Lock-Condition的结构,synchronized方法有一些局限性(比如对象锁可以被外界获取)。从使用上看,应该优先使用java.util.concurrent包下的类型完成同步任务,其次是考虑同步方法,因为它足够简单。最后,仅当目标功能的复杂性需要Lock-Condition时才去使用它。

@

同步的设计满足“木桶原理”,必须让所有的访问入口都加以同步,否则同步是不完整的无效的。

@

锁用来管理那些试图进入synchronized方法的线程,条件用来管理那些调用wait的线程。

  • 使用volatile关键字修饰变量后,它保证了多个线程访问的变量是内存中的同一个(线程可以在寄存器中保持变量的副本)。这样,原子操作(读取和写入)就“自然得到同步”了。不过非原子操作(!,n++,n=n+1)是无法被同步的,例如下面的方法flipCanceled是非同步的:
volatile boolean canceled;
volatile int count;
public void flipCanceled() {
  // 读取 + 写入 2个操作
  canceled = !canceled;
  count = count + 1;
}

“简单变量的同步”可以使用java.util.concurrent.atomic下的类型完成,其中使用了很多机器指令而不是锁完成同步。

volatile本身不提供同步,它修饰变量,保证多个线程访问同一个内存。而锁/synchronized保护代码块的同步执行。

  • 死锁
    锁和条件都有可能引起死锁,由于锁的竞争而引起的死锁从代码的同步设计上分析排查来避免。而条件问题可能和运行时的数据相关,最终所有线程都在等待条件,这样的bug更需要仔细设计来避免。

建议使用notifyAll、signalAll而避免notify、signal的使用。tryLock、带超时限制的tryLock、await也可以很大程度上避免死锁。相比lock方法的阻塞性质——无法被interrupted,tryLock和await都可以被中断来解除死锁。

  • 线程局部变量
    对非线程安全的对象进行同步会降低效率,线程安全的对象往往也很低效。对应一个被共享访问的变量,可以使用同步来保证安全,或者不需要共享时替换为在代码块中使用局部变量,这又带来了速度和内存上的消耗,而ThreadLocal 可以为每个线程提供独立的一份变量。

虽然可以自己设计线程类来保持需要的变量,不过好处是明显的,在完成针对不同线程创建不同变量对像的目标时,变量像普通的成员变量那样被定义,而不需要干涉具体的线程类。

  • 读写锁
    读取频繁,写较少。并发读取,互斥写入。ReentrantReadWriteLock。

  • 阻塞队列
    Lock、Condition是同步机制的底层工具,阻塞队列可以以数据结构的形式在高层次上完成多个线程之间的协同。生产者线程和消费者线程分别存放和读取队列中的任务数据,在队列满和空的时候阻塞。

优先使用java.util.concurrent包下提供了常用集合数据结构的线程安全的版本。Collections.synchronized系列静态方法可以将已有的非线程安全的集合包装为线程安全的版本。

高级类型

详细内容在用到时查阅包java.util.concurrent的API package summary文档。

任务和结果表示

  • Callable
  • Future
  • FutureTask

执行器

  • Interfaces
    Executor is a simple standardized interface for defining custom thread-like subsystems, including thread pools, asynchronous I/O, and lightweight task frameworks. Depending on which concrete Executor class is being used, tasks may execute in a newly created thread, an existing task-execution thread, or the thread calling execute, and may execute sequentially or concurrently. ExecutorService provides a more complete asynchronous task execution framework. An ExecutorService manages queuing and scheduling of tasks, and allows controlled shutdown. The ScheduledExecutorService subinterface and associated interfaces add support for delayed and periodic task execution. ExecutorServices provide methods arranging asynchronous execution of any function expressed as Callable, the result-bearing analog of Runnable. A Future returns the results of a function, allows determination of whether execution has completed, and provides a means to cancel execution. A RunnableFuture is a Future that possesses a run method that upon execution, sets its results.
  • Implementations
    Classes ThreadPoolExecutor and ScheduledThreadPoolExecutor provide tunable, flexible thread pools. The Executors class provides factory methods for the most common kinds and configurations of Executors, as well as a few utility methods for using them. Other utilities based on Executors include the concrete class FutureTask providing a common extensible implementation of Futures, and ExecutorCompletionService, that assists in coordinating the processing of groups of asynchronous tasks.

Class ForkJoinPool provides an Executor primarily designed for processing instances of ForkJoinTask and its subclasses. These classes employ a work-stealing scheduler that attains high throughput for tasks conforming to restrictions that often hold in computation-intensive parallel processing.

主要类型

  • Executors
public static ExecutorService newFixedThreadPool(int nThreads);
public static ExecutorService newCachedThreadPool();
public static ExecutorService newSingleThreadExecutor();
  • Executor
public interface Executor {
    void execute(Runnable command);
}
  • ExecutorService
    public interface ExecutorService extends Executor.
  • AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService
shutdown
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
invokeAll、invokeAny
  • ThreadPoolExecutor
    ThreadPoolExecutor extends AbstractExecutorService.

使用步骤

1)调用Executors的newXxxThreadPool得到线程池。
2)调用submit提交Runnable或Callable。
3)保存返回的Future。
4)没有后续任务执行时shutdown。

同步器

线程同步问题有一些典型的称作同步器的数据结构,包括:

  • Semaphore is a classic concurrency tool.
  • CountDownLatch is a very simple yet very common utility for blocking until a given number of signals, events, or conditions hold.
  • A CyclicBarrier is a resettable multiway synchronization point useful in some styles of parallel programming.
  • A Phaser provides a more flexible form of barrier that may be used to control phased computation among multiple threads.
  • An Exchanger allows two threads to exchange objects at a rendezvous point, and is useful in several pipeline designs.

资料

  • Java核心技术 卷1 基础知识 原书第9版

(本文是由Atom编写 10/18/2016 6:28:00 PM)

posted @ 2017-01-12 11:43  everhad  阅读(137)  评论(0编辑  收藏  举报