第一章介绍
(By wind5shy:http://blog.csdn.net/wind5shy)
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动。简单地说就是程序的一次执行,有自己独立的资源(内存空间,文件句柄等)。
线程:进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程共享所属进程范围内的资源,同时有自己的程序计数器、栈和本地变量(一般为代码中的局部变量)。同一
进程中的线程访问相同的变量,并从同一个堆中分配对象。
线程的优点
-
提高系统吞吐量,有效利用多处理。
-
将同时处理多种任务类型的程序变成顺序处理相同任务的程序:线程只处理一种任务类型,将不同类型的任务交给相应的线程处理,线程在特定点上同步。
-
使异步事件处理更简单。
-
使用户界面的响应性更好。
线程的风险
-
安全危险:允许多线程访问和修改相同的变量,给顺序编程模型引入了一些非顺序因素。在缺少同步的时候,编译器、硬件和运行时事实上对事件和活动顺序是很随意的。
-
活跃度的危险:安全意味着“坏事(不希望的事)没有发生”,活跃度则关注“好事(希望的事)发生了”。如果一个活动进入某种它永远无法再继续执行的状态时,称为活跃度失败,如死锁、饥饿等。
-
性能危险:活跃度关心好事是否发生,性能则关心好事多快发生。上下文切换(线程调度)和线程同步数据带来巨大的性能开销。
线程安全的传递性
通过框架调用应用程序的组件,不仅框架代码需要线程安全,而且所有代码路径上都需要线程安全。
(By wind5shy:http://blog.csdn.net/wind5shy)
第一部分基础
第二章线程安全
(By wind5shy:http://blog.csdn.net/wind5shy)
状态
对象的状态包含了任何对它外部可见行为产生影响的数据,即是说对象的状态改变则其对外暴露的行为也会改变。线程安全就是对状态访问的管理。状态一般是共享的、可变的。共享是指一个变量可以被多个线程访问;可变是指变量的值在其生命周期内可以改变。
竞争条件
多个进程并发访问和操作同一数据且执行结果与访问的特定顺序有关,称为竞争条件。
原子操作
操作的中间状态对观察者(其他线程)不可见,即观察者发现操作要么是未进行的状态,要么是完成后的状态。同时为了保护状态的一致性,在单一的原子操作中要更新所有相关的状态变量,保持状态变量之间的约束。
内部锁
又叫监视器锁,对应synchronized关键字。线程进入synchronized块前获得内部锁,放弃对synchronized块控制(正常退出代码、wait()等)时释放锁。
内部锁为互斥锁。
重进入
线程可以再次获取已经占有的锁,典型情况为synchronized块的嵌套。
实现是为锁关联一个请求计数和占有线程。计数器为0说明锁未被占有。线程占有该锁时,JVM记录占有线程并将计数器+1,该线程再次占有(进入一层synchronized块),计数器再+1,线程退出一层synchronized块,计数器-1。
用锁来保护状态
每个可被多个线程访问的可变状态变量对于所有访问它的线程都只能占有同一个锁,即这个锁是唯一而且确定的。对于每一个涉及多个变量的约束,需要用同一个锁保护其所有变量。
性能
有些耗时的计算或操作,如网络或控制台I/O,执行时不要占有锁。
(By wind5shy:http://blog.csdn.net/wind5shy)
第三章共享对象
(By wind5shy:http://blog.csdn.net/wind5shy)
非原子的64位操作
对于非volatile的long和double变量(64位),JVM可以将64位的读或写分为两个32位的操作,如果不同步,在线程间读写时可能出现高32位和低32位分别属于同一变量的不同值的情况。即对long和double变量的读写不是原子的。
锁和可见性
内部锁可以保证获得一个内部锁的线程对锁保护的变量的影响可以被下一个获得该锁的线程发现。所以要求变量由同一个锁保护,访问变量的线程由同一个锁同步。否则如果某变量由A、B两个锁保护,那么占有A的线程对变量进行的修改并不能通知到占有B的线程。
volatile变量
可以保证可见性,但无原子性,所以一般当作标识完成、中断、状态的标记使用。
使用条件
-
写入变量时并不依赖变量的当前值;或能够保证只有单一的线程修改变量的值。
-
变量不需要与其他的状态变量共同参与不变约束。
-
访问变量时,没有其他的原因需要加锁。
关于volatile的更详细深入的讨论见:《Java理论与实践:正确使用volatile变量》(http://blog.csdn.net/wind5shy/archive/2010/03/17/5387773.aspx)
发布和逸出
发布一个对象即使对象能够被当前范围外的代码所用,即把对象的引用传给外部。发布一个对象,同时也发布了该对象所有非私有域所引用的对象。
典型的逸出
-
发布数组的引用。外界可以通过数组中保持的每个对象的引用对相应的对象进行修改。
-
发布内部类实例。外界可以通过内部类实例的引用获得外部类的引用。
-
在构造器中this逸出。构造器是用来保证创建对象时的约束,通过this获得对象是未完全构造、不正确的。典型的例子是在构造器中创建一个线程并启动,这样新的线程在所属对象构造完成前就能看到它。构造器中可以创建线程,但不要启动,而是发布一个方法来启动。在构造器中调用可被override的方法也会导致this逸出。
线程封闭
数据不共享,只在单线程中使用。Java对于本地基本变量是线程封闭的,但对对象引用不是。在方法中创建的对象在方法调用结束后依然存在,要防止这些引用因为一些错误的方式从方法中逸出。
ThreadLocal:线程本地变量,封装一个变量,为每个使用它的线程单独创建并维护这个变量的独立副本,提供set()和get()供线程对其关联的变量副本进行访问。作用:1.针对可变singleton或全局变量的同步。2.为每个需要临时对象的线程都分配一个临时对象使用,无需手动逐个分配。
不可变性
不可变对象条件
-
状态不能在创建后被修改。
-
所有域都是final且对象被正确创建(创建时没有this逸出)。
尽可能将所有域声明为final。
Java储存模型为共享不可变对象提供了特殊的初始化安全性保证,发布对象引用时没有同步也可以被安全地访问,只要对象满足上述条件。
不可变对象无需额外同步即可用于任意线程和发布。
正确创建的可变对象安全发布模式(符合任一条即可,用来保证对象在发布时的状态对所有线程的可见性)
-
通过静态初始化器初始化对象的引用。
-
把对象引用存储到volatile域或AtomicReference。
-
把对象引用存储到正确创建的对象的final域中。
-
把对象引用存储到由锁正确保护的域中。
线程安全容器满足了最后一条要求,提供如下线程安全保证:
-
Hashtable、Collections.synchronizedMap、ConcurrentMap中的key对象和value对象,可以安全地发布到从Map获得它们的任意线程中,包括直接获得和通过iterator获得。
-
Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、Collections.synchronizedList、Collections.synchronizedSet中的元素对象,可以安全地发布到从容器中获得它们的任意线程中(不包括通过iterator获得!)。
-
BlockingQueue或ConcurrentLinkedQueue中的元素对象,可以安全地发布到可以从Queue中获得它们的任意线程中。
高效不可变对象:本身可能可变,但其状态在发布后不会被修改的对象。
任何线程都可以在没有额外同步的情况下安全地使用一个安全发布的高效不可变对象。因为安全发布保证了对象在发布时状态的可见性,而发布之后对象状态不会改变,无需关心。
安全共享对象的常用策略
-
线程限制:对象被线程独占,且只能被占有的线程修改。
-
共享只读:可被多线程读,但任何线程都不能修改对象。
-
共享线程安全:线程安全对象在内部同步,线程无需额外同步即可通过接口随意访问。
-
被守护:对象只能通过特定锁来访问。
(By wind5shy:http://blog.csdn.net/wind5shy)
第四章组合对象
(By wind5shy:http://blog.csdn.net/wind5shy)
设计线程安全的类
状态空间:对象及其变量可能处于的状态范围。
对象的不变约束判断状态的合法性,如果某状态是非法的,则必须封装该状态下的状态变量以免暴露给客户从而使对象处于非法状态。
操作的后验条件检查状态转换的合法性,如果一个操作可能出现非常状态转换,则必须使该操作成为原子的。
实例限制
把对象A封装在另一个对象B内部,通过B来限制对A的访问。
Collections.unmodifiableMap()只是返回一个对自身结构“只读”的Map,即不能往Map里增删改key和value对象,但key和value对象本身状态仍然可以通过其引用而被改变。
(By wind5shy:http://blog.csdn.net/wind5shy)
第五章构建块
(By wind5shy:http://blog.csdn.net/wind5shy)
同步容器
同步容器的迭代器不是线程安全的(因为迭代是复合操作)。
标准容器的toString()实现会迭代容器中的每个元素,hashCode()和equals()也会间接地调用迭代,如把容器本身作为另一个容器的元素或key。containsAll()、removeAll()、retainAll()和把容器作为参数的构造器都会进行迭代。
并发容器
同步容器对容器的所有状态进行串行访问来实现线程安全,其在每个操作的执行期间都持有一个锁。
并发容器是为多线程并发访问而设计的。
ConcurrentHashMap:使用分离锁机制。任意数量的读线程可以并发地访问Map,读和写可以并发,有限数量的写线程也可以并发。对整个Map进行操作的方法,如size()和isEmpty()结果并不绝对精确。
同步Map是为独占访问加锁,只有需要独占访问的时候(比如对元素进行迭代)才使用同步Map,其他情况下都使用ConcurrentHashMap。
ConcurrentHashMap不能在独占访问中加锁,所以不能使用客户端加锁创建新的原子操作。
CopyOnWriteArrayList、CopyOnWriteArraySet:采用“写入时复制”策略,适用于对容器的读操作远高于写操作的情况。对容器的写操作将导致的容器的复制,性能开销较大。
关于CopyOnWriteArrayList和Collections.synchronizedMap的具体比较可以看这个帖子:《CopyOnWriteArrayList与Collections.synchronizedMap性能比较》。
生产者-消费者模式
BlockingQueue:提供可阻塞的put()和take(),实现主要用于生产者-使用者队列。实现中ArrayBlockingQueue和LinkedBlockingQueue为FIFO队列,PriorityBlockingQueue为优先级排序队列,SynchronousQueue实际上不是队列,没有存储能力,不将任务放入队列而是直接把任务交给消费者,适合于消费者充足的情况。
双端队列:Deque,实现是ArrayDeque,非线程安全;BlockingDeque,实现是Linked BlockingDeque,线程安全。
阻塞和可中断的方法
中断
结束线程的阻塞状态,一般用来取消一个活动。
响应中断的两种基本选择
-
传递InterruptedException给调用者。抛出InterruptedException即可使方法成为响应中断的阻塞方法。
-
捕获InterruptedException,在当前线程中调用Thread.interrupt()设置线程状态为中断。
允许掩盖中断的唯一情况(即捕获InterruptedException后不做处理):扩展了Thread,并控制了所有处于调用栈上层的代码。
Synchronizer
同步器,协调线程。
闭锁(latch)
延迟线程的进度直到线程到达终止状态。闭锁像门:闭锁到达终点状态前,门一直关闭,没有线程可以通过;到达终点状态时,门开了,所有线程可以通过。闭锁到达终点状态后不能再改变状态,即门开了就不能再关。用来确保特定活动在其他活动完成后才进行,比如:
n 确保一个计算不会执行,直到它需要的资源被初始化。
n 确保一个服务不会开始,直到它依赖的服务都已经开始。
n 等待,直到活动的所有部分都为继续处理做好充分准备。
实现:
CountDownLatch:有一个计数器,表示需要等待的事件数;countDown()表示发生一个事件;await()让当前线程等待,直到计数器为0。
FutureTask:通过Callable(等价于一个可携带结果的Runnable)实现,有3个状态:等待、运行和完成。FutureTask进入完成状态后不会改变。get()依赖于前面的状态,如果完成,则立刻返回结果;否则会阻塞直到任务完成再返回结果或抛出异常。
信号量(Semaphore)
控制能够同时访问某特定资源的活动的数量,或同时执行某一给定操作的数量。
实现:
Semaphore:实现中没有真正的许可对象,而且Semaphore也没有真正向线程分配许可。可以把acquire()当作是消费一个许可,而release()是创建一个许可。
关卡(Barrier)
所有线程必须在一定时间内到达关口点,才能继续处理。通常用作一个步骤的计算可以并行完成,但要求所有处理者必须完成与一个步骤相关的工作后才能进入下一步。
实现:
CyclicBarrier:构造器中设置参与者数目,还可以传递一个Runnable以设置关卡行为(在通过关卡的时候由最后一个进入关卡的线程执行的动作);await()供参与者调用,如果所有参与者都调用await()则关卡通过,否则等待。
Exchanger:两方关卡,在关卡点双方交换数据。exchange()供两方调用,两方都调用后进行数据交换。一般用作缓存交换:双方都有两块或更多的缓存,一方作为生产者向自己的缓存中写数据,另一方作为消费者从自己的缓存中读数据,双方在关卡点进行缓存交换,用满缓存换空缓存。交换策略:1.写入满时或读出满时均交换,缺点是新数据到达的速度不可知的话有时延迟会比较久。2.缓存满了就交换,到达一定时间也交换。