Java并发那些事
之前整理了一些关于Java虚拟机的内容,而作为Java程序员,并发编程也是很重要的方面,下面就根据《Java并发编程实战》这本书做一些整理。
线程安全性
线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
对象的共享
可见性
我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态;并且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
加锁与可见性:加锁的含义不仅仅是局限于互斥行为,还包括内存可见性。
Volatile变量
用来确保将变量的更新操作通知到其他线程。编译器和运行时,会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。不会执行加锁操作,是一种比synchronized关键字更轻量级的同步机制。但是它不提供原子性。
当下面所有条件满足时,才应该使用volatile变量:
1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2.该变量不会与其他状态变量一起纳入不变性条件中
3.在访问变量时不需要加锁
线程封闭
一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭。在Swing以及JDBC connection中大量使用了线程封闭技术。
栈封闭:只能通过局部变量才能访问对象。而局部变量位于执行线程的栈中。缺点:维护困难,容易使对象逸出。
TheadLocal类:它使线程中的某个值与保存值的对象关联起来。当某个频繁执行的操作需要一个临时对象,而同时希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。Threadlocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
不变性
不可变对象一定是线程安全的。
当满足以下条件时,对象才是不可变的:
1.对象创建后,其状态就不能修改
2.对象的所有域都是final类型
3.对象时正确创建的(在对象的创建期间,this引用没有逸出)
基础构建模块
同步容器类
比如有Vector和Hashtable。它们实现线程安全的方式是:把它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
及时失败策略(fail-fast):当它们发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException
加锁是一种方案,但开销大;克隆也是一种方案,但容器过大时,开销也不小。
加锁有时也避免不了隐藏迭代器的情况。
并发容器
Java5提供了多种并发容器来改进同步容器的性能。
ConcurrentHashMap
采用了分段锁机制。包含16个锁的数组,每个锁保护散列桶的1/16.
返回的迭代器具有弱一致性,这意味着它可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
为了反映容器的并发特性,size和isEmpty这些方法的语言被略微减弱了,因为它们返回的只是一个估计值,结果有可能已经过期了。但由于返回值一直在变化,这些方法在并发环境下用处很小。我们会更关注get、put、containKey和remove等操作。
劣势:获得所有锁的开销更大,比如resize的时候
要确保锁上的竞争频率高于在锁保护的数据上发生竞争的频率。当每个操作都请求多个变量时,锁的粒度将很难降低。性能与可伸缩性的权衡。
每个分段都维护一个独立的计数器,调用size,返回值缓存到一个volatile变量,容器被修改则为-1,正值返回,负值需要重新计算。
CopyOnWriteArrayList
用于替代同步List
阻塞队列和生产者-消费者模式
BlockingQueue
同步工具类
闭锁
可以延迟线程的进度直到其到达终止状态。
CountDownLatch,初始化为一个正数,表示需要等待的事情数量。countDown方法递减计数器,表示有一个事情已经发生了,而await方法等待计数器达到零。
FutureTask可以做闭锁,它在Executor框架中表示异步任务,还可以用来表示一些时间较长的计算。
信号量:Counting Semaphore用来控制同时访问某个特定资源的操作数量。通过acquire和release来获取和释放信号量。
栅栏:与闭锁的区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待时间,而栅栏用于等待其他线程。await()会阻塞到所有线程都到达栅栏的位置。如果栅栏打开,则所有线程被释放;并且每个线程会拿到await返回的一个唯一的到达索引号,可以利用索引号选出一个领导线程,并在下一次迭代中由改领导线程执行一些特殊的工作。
任务执行
无限制创建线程的不足
1.线程生命周期的开销非常高
2.资源消耗:过多的线程,会产生大量空闲线程占用内存,给垃圾回收带来压力;线程竞争CPU资源也会带来其他的性能开销。
3.稳定性:实际上,可创建线程的数量上存在一个限制。
Executor框架
它为异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略;另外,它的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。
基于生产者-消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),执行任务的线程则相当于消费者(执行完这些工作单元)。将任务提交和任务执行解藕了。
线程池
好处:
1.分摊在线程创建和销毁过程中产生的巨大开销
2.当请求到达时,工作线程通常已经存在,因此不会有延迟,提高了响应性
3.保持处理器合理忙碌,避免线程过度竞争资源
通过Executors中的静态工厂方法来创建线程池:
1.newFixedThreadPool(int num):创建固定长度的线程池,满了以后规模不再变化。
2.newCachedThreadPool:创建一个可缓存的线程池,适合那些短任务;如果已有线程都在忙,会增加新线程;超过60s未工作的线程会被回收。
3.newSingleThreadExecutor:单线程,确保任务的顺序(FIFO、LIFO等)。
4.newScheduledThreadPool(int num):创建一个固定长度的线程池,而且是以延迟或定时的方式来执行任务。
Executor的生命周期
ExecutorService扩展了Executor接口,添加了一些用于生命周期管理的方法,同时还有一些用于任务提交的便利方法。
生命周期的三种状态:运行,关闭,已终止。
shutdown方法采取平缓关闭:不再接受新任务,等待已提交任务完成(包括还未开始执行的任务)。
shutdownNow方法采取粗暴的关闭:尝试取消所有运行的任务,不再启动队列中的未执行的任务。
携带结果的任务Callable与Future
Callable被认为是主入口点将返回一个值,并可能抛出一个异常。描述的是一种抽象的计算任务。
Future表示一个任务的生命周期,并提供响应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。
ExecutorService中所有submit方法都将返回一个Future。将Runnable或Callable提交给Executor,并得到一个Future用来获得任务的执行结果或取消任务。
还可以显示地为某个指定地Runnable或Callable实例化一个FutureTask。
取消与关闭
通常,中断是实现取消的最合理方式。
通过Future来实现取消。
线程池的使用
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。
如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争。
如果线程池过小,那么很多处理器没有被利用起来,从而降低了吞吐率。
配置 ThreadPoolExecutor
用来实现Executors里的一些工厂方法:
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockinQueue<Ruannable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...}
线程池的基本大小、最大大小和存活时间等因素共同负责线程的创建与销毁。
基本大小就是线程池的目标大小,即在没有任务执行时线程池的大小,只有工作队列满了才会创建超出这个数量的线程。
线程池的最大大小表示可同时活动的线程数量的上限。
如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列、有界队列和同步移交。
newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。线程都忙碌,在队列中等候,如果任务持续到达,并超过了线程池处理它们的速度,那么队列将无限制地增加。
一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生。需要队列的大小和线程池大小配合。
对于非常大的或者无界的线程池,可以通过SynchronousQueue来避免任务排队。它不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么将创建一个新的线程。否则根据饱和策略,这个任务将被拒绝。newCachedThreadPool就使用了这种机制。
只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。
饱和策略
当有界队列被填满后,饱和策略开始发挥作用。可以通过setRejectedExecutionHandler来修改。不同的饱和策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy。
AbortPolicy:默认的策略,抛出异常,自己写代码来处理
DiscardPolicy:会悄悄抛弃该任务
DiscardOldestPolicy:抛弃下一个将被执行的任务,然后尝试重新提交新任务。不要与优先队列一起使用。
CallerRunsPolicy:将某些任务回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。
线程工厂
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。许多情况下都需要使用定制的线程工厂方法。
扩展ThreadPoolExecutor
在子类中改写方法beforeExecute,afterExecute和terminated。
死锁的避免与诊断
如果每次至多只能获得一个锁,那么就不会产生锁顺序死锁。
显示使用Lock类中的定时tryLock功能。内置锁,没有获得锁会一直等下去。
性能与可伸缩性
可伸缩性指的是增加资源时,程序的吞吐量或处理能力相应地增加。
Amdahl定律描述:在增加计算资源地情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占比重。
减少锁地竞争
1.减少锁地持有时间
2.减少锁地粒度
3.降低锁地请求频率
4.使用带有协调机制的独占锁,这些机制允许更高的并行性。
显示锁
可重入的加锁语义,灵活性。
轮询锁与定时锁:tryLock方法。
轮询锁,如果不能获得所需要的锁,那么可以使用可轮询的锁获取方式,从而尝试获得控制权。它会释放已经获得的锁,然后重新尝试获取所有锁。
定时锁,如果操作不能在指定的时间内给出结果,那么就是使程序提前结束。
可中断的锁获取操作。
公平性。
显示的Condition对象。