Java中的各种锁

java中的锁大概可以分为以下几种:

  • 乐观锁与悲观锁
  • 独享锁(排他锁、独占锁)与共享锁
  • 公平锁与非公平锁
  • 偏向锁、轻量级锁、重量级锁
  • 可重入锁
  • 自旋锁
  • 分段锁

一、乐观锁与悲观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并非的可能性低,每次去获取数据的时候都认为数据不会被别人修改,但是会在更新的数据的时候判断一下在此期间别人有没有修改过这个数据,一般采取在写时先读出当前的版本号,比较跟上一次的版本号,如果一致才进行更新。java中的乐观锁基本都是基于CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一致,一致则更新,否则失败。例如JUC(java.util.concurrent)Atomic包下的如AtomicInteger这些类都是CAS实现。

悲观锁就是悲观的思想,认为写发生的情况比较频繁,遇到并发的可能性很高,每次去获取数据都悲观的认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想操作这个数据就会被block(堵塞)直到获取到锁,java中的Synchronized就是悲观锁,AQS(抽象的队列同步器)框架下的锁(如RetreenLock)则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁。

二、独享锁(排他锁、独占锁)与共享锁

独享锁每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独享锁是一种悲观保守的加锁策略,避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

共享锁是一种乐观锁,放宽了加锁策略,允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock。

三、公平锁与非公平锁

公平锁(Fair)是指在加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得到锁。

非公平锁(Nonfair)是指加锁时不考虑排队等待的问题,直接尝试获取锁,如果获取到了当前线程就不用挂起堵塞,减少上下文切换消耗,性能比公平锁要高,公平锁需要在多核的情况下维护一个队列,获取不到则自动到队尾等待。

Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

四、偏向锁、轻量级锁、重量级锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

 

偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀升级为轻量级锁。

 

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

 

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

 

五、可重入锁

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

在JAVA环境下ReentrantLock和synchronized都是可重入锁。

六、自旋锁

自旋锁是指如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

七、分段锁

分段锁并非一种实际的锁,而是把一把锁替换成多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。

 

posted @ 2021-02-28 12:15  RunTheNight  阅读(969)  评论(0编辑  收藏  举报