深入浅出 Java Concurrency (15): 锁机制 part 10 锁的一些其它问题[转]

主要谈谈锁的性能以及其它一些理论知识,内容主要的出处是《Java Concurrency in Practice》,结合自己的理解和实际应用对锁机制进行一个小小的总结。

首先需要强调的一点是:所有锁(包括内置锁和高级锁)都是有性能消耗的,也就是说在高并发的情况下,由于锁机制带来的上下文切换、资源同步等消耗是非常可观的。在某些极端情况下,线程在锁上的消耗可能比线程本身的消耗还要多。所以如果可能的话,在任何情况下都尽量少用锁,如果不可避免那么采用非阻塞算法是一个不错的解决方案,但是却也不是绝对的。

内部锁

Java语言通过synchronized关键字来保证原子性。这是因为每一个Object都有一个隐含的锁,这个也称作监视器对象。在进入synchronized之前自动获取此内部锁,而一旦离开此方式(不管通过和中方式离开此方法)都会自动释放锁。显然这是一个独占锁,每个锁请求之间是互斥的。相对于前面介绍的众多高级锁(Lock/ReadWriteLock等),synchronized的代价都比后者要高。但是synchronized的语法比较简单,而且也比较容易使用和理解,不容易写法上的错误。而我们知道Lock一旦调用了lock()方法获取到锁而未正确释放的话很有可能就死锁了。所以Lock的释放操作总是跟在finally代码块里面,这在代码结构上也是一次调整和冗余。另外前面介绍中说过Lock的实现已经将硬件资源用到了极致,所以未来可优化的空间不大,除非硬件有了更高的性能。但是synchronized只是规范的一种实现,这在不同的平台不同的硬件还有很高的提升空间,未来Java在锁上的优化也会主要在这上面。

性能

由于锁总是带了性能影响,所以是否使用锁和使用锁的场合就变得尤为重要。如果在一个高并发的Web请求中使用了强制的独占锁,那么就可以发现Web的吞吐量将急剧下降。

为了利用并发来提高性能,出发点就是:更有效的利用现有的资源,同时让程序尽可能的开拓更多可用的资源。这意味着机器尽可能的处于忙碌的状态,通常意义是说CPU忙于计算,而不是等待。当然CPU要做有用的事情,而不是进行无谓的循环。当然在实践中通常会预留一些资源出来以便应急特殊情况,这在以后的线程池并发中可以看到很多例子。

线程阻塞

锁机制的实现通常需要操作系统提供支持,显然这会增加开销。当锁竞争的时候,失败的线程必然会发生阻塞。JVM既能自旋等待(不断尝试,知道成功,很多CAS就是这样实现的),也能够在操作系统中挂起阻塞的线程,直到超时或者被唤醒。通常情况下这取决于上下文切换的开销以及与获取锁需要等待的时间二者之间的关系。自旋等待适合于比较短的等待,而挂起线程比较适合那些比较耗时的等待。

挂起一个线程可能是因为无法获取到锁,或者需要某个特定的条件,或者耗时的I/O操作。挂起一个线程需要两次额外的上下文切换以及操作系统、缓存等多资源的配合:如果线程被提前换出,那么一旦拿到锁或者条件满足,那么又需要将线程换回执行队列,这对线程而言,两次上下文切换可能比较耗时。

锁竞争

影响锁竞争性的条件有两个:锁被请求的频率和每次持有锁的时间。显然当而这二者都很小的时候,锁竞争不会成为主要的瓶颈。但是如果锁使用不当,导致二者都比较大,那么很有可能CPU不能有效的处理任务,任务被大量堆积。

所以减少锁竞争的方式有下面三种:

  1. 减少锁持有的时间
  2. 减少锁请求的频率
  3. 采用共享锁取代独占锁

死锁

如果一个线程永远不释放另外一个线程需要的资源那么就会导致死锁。这有两种情况:一种情况是线程A永远不释放锁,结果B一直拿不到锁,所以线程B就“死掉”了;第二种情况下,线程A拥有线程B需要的锁Y,同时线程B拥有线程A需要的锁X,那么这时候线程A/B互相依赖对方释放锁,于是二者都“死掉”了。

还有一种情况为发生死锁,如果一个线程总是不能被调度,那么等待此线程结果的线程可能就死锁了。这种情况叫做线程饥饿死锁。比如说在前面介绍的非公平锁中,如果某些线程非常活跃,在高并发情况下这类线程可能总是拿到锁,那么那些活跃度低的线程可能就一直拿不到锁,这样就发生了“饥饿死”。

避免死锁的解决方案是:尽可能的按照锁的使用规范请求锁,另外锁的请求粒度要小(不要在不需要锁的地方占用锁,锁不用了尽快释放);在高级锁里面总是使用tryLock或者定时机制(这个以后会讲,就是指定获取锁超时的时间,如果时间到了还没有获取到锁那么就放弃)。高级锁(Lock)里面的这两种方式可以有效的避免死锁。

活锁

活锁描述的是线程总是尝试某项操作却总是失败的情况。这种情况下尽管线程没有被阻塞,但是人物却总是不能被执行。比如在一个死循环里面总是尝试做某件事,结果却总是失败,现在线程将永远不能跳出这个循环。另外一种情况是在一个队列中每次从队列头取出一个任务来执行,每次都失败,然后将任务放入队列头,接下来再一次从队列头取出任务执行,仍然失败。

还有一种活锁方式发生在“碰撞协让”情况下:两个人过独木桥,如果在半路相撞,双方礼貌退出去然后再试一次。如果总是失败,那么这两个任务将一直无法得到执行。

总之解决锁问题的关键就是:从简单的开始,先保证正确,然后再开始优化。

时间: 2024-10-25 17:46:15

深入浅出 Java Concurrency (15): 锁机制 part 10 锁的一些其它问题[转]的相关文章

深入浅出 Java Concurrency (25): 并发容器 part 10 双向并发阻塞队列 BlockingDeque[转]

这个小节介绍Queue的最后一个工具,也是最强大的一个工具.从名称上就可以看到此工具的特点:双向并发阻塞队列.所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程,这里说的队列是指支持FIFO/FILO实现的链表. 首先看下LinkedBlockingDeque的数据结构.通常情况下从数据结构上就能看出这种实现的优缺点,这样就知道如何更好的使用工具了. 从数据结构和功能需求上可以得到以下结论: 要想支持阻塞功能,队列的容量一定是固定的,否则无法在

深入浅出 Java Concurrency - 目录 [转]

这是一份完整的Java 并发整理笔记,记录了我最近几年学习Java并发的一些心得和体会. J.U.C 整体认识 原子操作 part 1 从AtomicInteger开始 原子操作 part 2 数组.引用的原子操作 原子操作 part 3 指令重排序与happens-before法则 原子操作 part 4 CAS操作 锁机制 part 1 Lock与ReentrantLock 锁机制 part 2 AQS 锁机制 part 3 加锁的原理 (Lock.lock) 锁机制 part 4 锁释放与

深入浅出 Java Concurrency (17): 并发容器 part 2 ConcurrentMap (2)

本来想比较全面和深入的谈谈ConcurrentHashMap的,发现网上有很多对HashMap和ConcurrentHashMap分析的文章,因此本小节尽可能的分析其中的细节,少一点理论的东西,多谈谈内部设计的原理和思想. 要谈ConcurrentHashMap的构造,就不得不谈HashMap的构造,因此先从HashMap开始简单介绍. HashMap原理 我们从头开始设想.要将对象存放在一起,如何设计这个容器.目前只有两条路可以走,一种是采用分格技术,每一个对象存放于一个格子中,这样通过对格子

深入浅出 Java Concurrency (9): 锁机制 part 4[转]

本小节介绍锁释放Lock.unlock(). Release/TryRelease unlock操作实际上就调用了AQS的release操作,释放持有的锁. public final boolean release(int arg) {    if (tryRelease(arg)) {        Node h = head;        if (h != null && h.waitStatus != 0)            unparkSuccessor(h);       

深入浅出 Java Concurrency (6): 锁机制 part 1[转]

前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明.从这一章开始花少量的篇幅谈谈锁机制. 上一个章节中谈到了锁机制,并且针对于原子操作谈了一些相关的概念和设计思想.接下来的文章中,尽可能的深入研究锁机制,并且理解里面的原理和实际应用场合. 尽管synchronized在语法上已经足够简单了,在JDK 5之前只能借助此实现,但是由于是独占锁,性能却不高,因此JDK 5以后就开始借助于JNI来完成更高级的锁实现. JDK 5中的锁是接口java.util.

深入浅出 Java Concurrency(二)—锁机制(一)

前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明.从这一章开始花少量的篇幅谈谈锁机制. 上一个章节 中谈到了锁机制,并且针对于原子操作谈了一些相关的概念和设计思想.接下来的文章中,尽可能的深入研究锁机制,并且理解里面的原理和实际应用场合. 尽管synchronized在语法上已经足够简单了,在JDK 5之前只能借助此实现,但是由于是独占锁,性能却不高,因此JDK 5以后就开始借助于JNI来完成更高级的锁实现. JDK 5中的锁是接口java.util

深入浅出 Java Concurrency (12): 锁机制 part 7 信号量(Semaphore)[转]

Semaphore 是一个计数信号量.从概念上讲,信号量维护了一个许可集.如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可.每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者.但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动. 说白了,Semaphore是一个计数器,在计数器不为0的时候对线程就放行,一旦达到0,那么所有请求资源的新线程都会被阻塞,包括增加请求到许可的线程,也就是说Semaphore不是可重

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)[转]

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能

深入浅出 Java Concurrency (8): 锁机制 part 3[转]

接上篇,这篇从Lock.lock/unlock开始.特别说明在没有特殊情况下所有程序.API.文档都是基于JDK 6.0的. public void java.util.concurrent.locks.ReentrantLock.lock() 获取锁. 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1. 如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回. 如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于