锁机制-使用锁带来的一些问题

首先强调一点:全部锁(包括内置锁和高级锁)都是有性能消耗的,在高并发的状况下,使用锁可能比线程自己的消耗要大,因为锁机制带来的上下文切换,资源同步等消耗,因此若是可能,在任何状况下都应该少使用锁,若是不可避免,采用非阻塞算法是一个不错的解决方案。算法

内部锁

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

性能

因为锁老是带了性能影响,因此是否使用锁和使用锁的场合就变得尤其重要。若是在一个高并发的Web请求中使用了强制的独占锁,那么就能够发现Web的吞吐量将急剧降低。缓存

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

线程阻塞

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

挂起一个线程多是由于没法获取到锁,或者须要某个特定的条件,或者耗时的I/O操做。挂起一个线程须要两次额外的上下文切换以及操做系统、缓存等多资源的配合:若是线程被提早换出,那么一旦拿到锁或者条件知足,那么又须要将线程换回执行队列,这对线程而言,两次上下文切换可能比较耗时。并发

如何减小上下文切换

  • 无锁并发编程。多线程竞争锁时,会引发上下文切换,因此多线程处理数据时,能够用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不一样的线程处理不一样段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不须要加锁。
  • 使用最少线程。避免建立不须要的线程,好比任务不多,可是建立了不少线程来处理,这样会形成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

锁竞争

影响锁竞争的条件有两个高并发

  • 持有锁的时间
  • 请求锁的频率

显然当这二者都很小的时候,锁竞争不会成为主要的瓶颈,可是若是使用不当,致使两者都比较大,那么CPU可能不能有效的处理任务,致使任务大量堆积性能

因此减小锁竞争的方法有如下三个:优化

  • 减小持有锁的时间
  • 减小请求锁的频率
  • 使用共享锁代替独占锁

死锁

  • 若是一个线程永远不释放另外一个线程所须要的资源就会致使死锁,这有两种状况,
  1. 线程A永远不释放锁,致使B永远拿不到锁,因此线程B死掉
  2. 线程A须要线程B持有的锁,线程B拥有线程A须要的锁,致使线程AB互相等待
  • 还有一种状况发生死锁,若是一个线程永远不能被调度,那么等待此线程结果的线程可能就死掉了,这种状况叫作饥饿死锁,好比说在非公平锁中,若是某些线程很是活跃,在高并发的状况下这类线程总能拿到锁,那么活跃度低的线程可能永远拿不到锁,这样就发生了饥饿死

死锁产生的4个必要条件

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已得到的资源保持不放。
  • 不剥夺条件:进程已得到的资源在未使用完以前,不能剥夺,只能在使用完时由本身释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

预防死锁的解决方案是:

  • 尽量的按照锁的规范使用锁,另外请求锁的粒度要小
  • 在高级锁里面使用tryLock或者定时机制,(指定获取锁超时的时间,若是时间到了还没获取到就放弃)
  • 以肯定的顺序得到锁,针对两个特定的锁,开发者能够尝试按照锁对象的hashCode值大小的顺序,分别得到两个锁,这样锁老是会以特定的顺序得到锁,那么死锁也不会发生。问题变得更加复杂一些,若是此时有多个线程,都在竞争不一样的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就没法知足要求了,这个时候开发者可使用银行家算法,全部的锁都按照特定的顺序获取,一样能够防止死锁

银行家算法

  • 银行家算法:首先须要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源状况。所以,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会致使死锁。当进程请求一组资源时,假设赞成该请求,从而改变了系统的状态,而后肯定其结果是否还处于安全状态。若是是,赞成这个请求;若是不是,阻塞该进程知道赞成该请求后系统状态仍然是安全的。

死锁恢复

  • 利用抢占恢复
  • 杀死进程

活锁

线程老是尝试某项操做却老是失败的状况这种状况下线程没有被阻塞,可是任务不能被执行。操作系统

  • 好比在一个死循环里老是尝试作某件事,结果却老是失败,线程将永远没法跳出这个循环
  • 在一个队列中,每次从队列头取出一个任务来执行,每次都失败,而后将任务放入队列头,继续失败
  • 在碰撞协议状况下,线程优先度低的线程得不到执行,也会致使活锁发生。
相关文章
相关标签/搜索