第21章 并发

本章所讲的并发是通向高级主题的中介,但即使融汇贯通本章内容,也不能说自己是一个优秀的并发程序员。学习并发编程就像进入一个全新的领域


源码分析参见:
相关文章:第十二章:并发编程
详细并发教程参见:《Java并发编程实战》

并发通常是为了提高运行在单处理器上的程序性能,而不是直观上的多处理器上。乍看可响应起来有悖常理,多个程序一起运行,不停地切换上下文,看起来可能比程序顺序执行要慢?造成这个错觉的原因就是因为阻塞,程序在执行过程中并不是百分之百使用CPU,因此空闲的那段时间(也就是阻塞)时浪费的,因此使用并发来编写程序时,就可以切换到其他程序继续执行

使用并发可以实现具有可响应的用户界面。某些编程语言被设计为可以将并发任务彼此隔离,这些语言称为函数型语言,其中每个函数调用都不会产生任何副作用,并且可以当做独立的任务来驱动。在面对游戏仿真、分布式系统时,并发机制都能提供更好的编码设计

基本线程机制

线程可以驱动任务,通过实现Runnable接口并编写run()方法来定义任务。要实现线程行为,就要显式的把任务附着到线程上去。通过向Thread类构造器传入一个Runnable对象,并调用start()方法为该线程执行必要的初始化操作,然后自动调用run()方法,以便在这个新线程中启动该任务。调用start()后悔迅速返回,但由于Thread对象都注册了自己,所以会继续存在,并独立运行,直到任务退出,垃圾回收器才会清除它

java.util.concurrent包中的执行器可以替我们管理Thread对象,从而简化并发编程,Executor在客户端和任务执行之间提供了一个间接层,由这层来执行任务。ExecutorService(具有服务生命周期的Executor,即线程池,实现该接口的类有ThreadPoolExecutor和ScheduledThreadPoolExecutor)知道如何构建恰当的上下文来执行Runnable对象。Java还提供了一个Executors工厂类,它可以帮助我们很方便的创建各种类型ExecutorService线程池,以下方法都是静态创建对象

  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,并一次性预先执行代价高昂的线程分配,需要线程的事件直接从池中获取,超出的线程会在队列中等待,
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行
  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

上面提到具有服务生命周期的ExecutorService指的是其具有shutdown()方法,可以防止新任务被提交到这个Executor。在所有类型的线程池中,现有线程都可能会被自动复用

Java 1.5中引入的Callable接口是具有类型参数的泛型,它和Runnable接口的区别就在于完成任务时有返回值,而且必须使用ExecutorService.submit()方法来调用实现好的call(),submit()会返回一个Future对象,其代表了异步计算结果,并具有方法来获取get()、检查isDone()结果

  • sleep()可以使任务暂停指定时间单元
  • getPriority() & setPriority()可以读取和修改现有线程的优先级,通常来说修改优先级都是种错误
  • yield()让步,当工作做得差不多时,可以通过该方法暗示让别的线程使用CPU

后台线程

daemon指在程序运行的时候在后台提供一种通用服务的线程,这种线程不是程序不可或缺的部分,这意味着当所有非后台线程结束时,程序就终止,并杀死所有后台进程。通过setDaemon() & isDaemon()可以设置或判断后台线程

后台线程中run()中的finally块语句不会得到执行,这意味着后台线程的finally语句得不到保证,因此一旦线程都退出,JVM就会立即关闭所有后台进程

加入线程

在Java中,Thread类本身并不执行任何操作,它只是驱动赋予它的任务。从物理层面看,把任务从线程中分离出来就变得有意义,因为创建线程的代价很高昂。Java的线程机制是基于C的pthread机制,这种低级特性部分渗透到了Java的实现中,下面就是一个例子

一个线程可以在其他线程上调用join(),其效果是等待一段时间直到第二个线程结束才继续执行,源码给出的注释是Waits for this thread to die,命名来源于posix标准,子线程join到主线程(启动程序的线程,比如c语言执行main函数的线程),阻塞线程仅仅是一个表现,而非目的。其目的是等待当前线程执行完毕后,”计算单元”与主线程汇合,即主线程与子线程汇合之意。对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往先于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再执行并结束自己,这个时候就要用到join()方法

异常

由于线程本质特性,不能捕获从线程中逃逸的异常,一旦逃出任务的run()方法,就会向外传播到控制台,Java 1.5之后就可以用Executor解决这个问题。Thread.UncaughtExceptionHandler接口允许在Thread对象上都附着一个异常处理器,Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用

同步

一般解决并发模式中共享资源竞争问题,都是采取序列化访问共享资源的方案,这意味着在给定时刻只允许一个任务访问共享资源。Java提供synchronized,为防止资源冲突提供了内置支持,当任务执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。共享资源都会被包装到一个对象中,然后把所有要访问这个资源的方法都标记为synchronized,所有对象都会含有单一的(监视器),当在对象上调用任意标有synchronized的方法时,该对象就会上锁,其他调用该对象的方法都阻塞直到上一个方法释放锁

一个任务可以多次获得对象的锁,即如果一个方法在同一个对象上调用了第二个方法,后者又调用同一个对象的另一个方法。JVM会负责追逐对象被加锁的次数

java.util.concurrent.locks提供了显式的互斥机制,Lock对象必须被显式的创建、锁定和释放。在使用这种方法时,要用到try-finally语句,这意味着有机会做任何清理工作,以维护系统使其处于良好状态

Brain同步规则:如果正在写一个常量,他可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,就要使用同步,并且要使用相同的锁

原子性与易变性

原子操作是不能被线程调度机制中断的操作,一旦操作开始,它一定可以在可能发生的上下文切换之前执行完毕。不要为了避免同步而使用原子性

JVM对于long double变量的读写和写入当做两个分离的32位操作来执行,这产生了在读取和写入操作中间发生上下文切换时,会导致不同的任务可以看到不正确结果,如果使用volatile关键字,就能获得原子性

对于多核处理器系统来说,任务做出的修改对其他任务可能是不可视的(例如修改只是暂时保存在本地处理器的缓存中),volatile保证了修改的应用可视性,对volatile域的修改,其他读操作都能看到,volatile域会立即被写入主存中。一个会被多个任务访问的域,就应该是volatile,或者由同步来访问,因为同步也会导致向内存中刷新

原子性和易变性是不同的概念,在Java中自增和自加都不是原子性的??

临界区(同步控制块):防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,这种方法可以使多个任务访问对象的时候性能得到显著提高

防止共享资源冲突的第二种方法是根除对变量的共享线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储,这使得状态和线程关联起来

终结任务

线程协作

死锁