• 欢迎来到本博客,希望可以y一起学习与分享

Java并发小结(二) — 常见的锁分类

Java benz 来源:Java并发小结(二) -- 常见的锁分类 4年前 (2018-04-09) 120次浏览 0个评论 扫描二维码

常见的锁分类及概念

  • 公平锁/非公共锁
  • 共享锁/独占锁
  • 可重入锁/不可重入锁
  • 读写锁
  • 偏向锁/轻量级锁/重量级锁;锁升级/锁降级
  • 锁消除/锁膨胀
  • 悲观锁/乐观锁

接下来我们来一个个分析下,到底这些概念到底是啥意思:

1. 公平锁/非公平锁

当多个线程同时获取一把锁时,JVM按照什么顺序去分配这些锁呢?公平性就是来区分这个顺序的。如果一个锁是公平锁,那么会按照线程入队的顺序来分配锁,不会出现插队的情况;如果一个锁是非公平锁,则允许插队,可以让优先级更高的线程获得锁。

非公平锁主要是为了提高吞吐率,按照《Java并发编程实战》的数据,公平性将性能降低了约两个数量级。

为什么插队行为可以提高吞吐率呢?主要原因是在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。公平锁在队列中有多个线程的情况下,队首的线程很可能是挂起的,所以这个延迟对公平锁产生了很大的影响;而非公平锁由于允许插队,很有可能队尾的线程还没有挂起,直接就可以获取锁,所以没有这个延迟,因此总的吞吐率会有很大的提升。

如果不需要保证线程的有序性,那么非公平锁可以优先考虑。但是非公平锁性能提升的同时,由于允许插队的存在,在一些极端情况下,可能会造成部分线程一直得不到锁,需要注意这个问题。

2. 共享锁/独占锁

一个锁是否只能有由一个线程持有? 共享锁,就是允许多个线程同时持有同一把锁,具有代表性的是CountDownLatch; 独占锁,一把锁只能同时被一个线程持有,比如synchronized内置锁。

3. 可重入锁/不可重入锁

可重入锁指的是ReentrantLock,一个线程在获取锁之后,可以不释放该锁,直接再次获取锁,获取多次后,需要对应多次释放。可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

不可重入锁指的在线程获取锁后,如果不释放该锁,该线程就不能再次获取该锁。

4. 读写锁

顾名思义,读写锁,内置了两把锁,一把读锁,一把写锁。J.U.C里读写锁的实现是ReentrantReadWriterLock,读写锁,从类名上我们可以看到这个读写锁的实现是可以重入的,这时里重入指的是读锁的重入。

读写锁的特性是,多个线程可以同时获取多个读锁;当获取写锁时,必须释放所有读锁;读锁可以升级为写锁;写锁不能降级为读锁。

读写锁于在多个线程能够同时读数据,在读写比较高的情况下,有比较好的性能。

读写锁内部实现基于AbstractQueuedSynchronizer。读锁与写锁都同时对应同一个AQS同步器。

AQS中有一个state字段(int类型,32位)用来描述有多少线程获持有锁。在独占锁中这个值通常是0或者1(如果是重入的就是重入的次数),在共享锁中就是持有锁的数量。很显然,读锁属于共享锁,写锁属于独占锁,那同一个状态怎么能同步处理共享锁与独占锁的两种不一致的逻辑呢?这篇文章给出了解释,在ReentrantReadWrilteLock里面将这个字段一分为二,高位16位表示共享锁的数量,低位16位表示独占锁的数量(或者重入数量)。

5. 偏向锁/轻量级锁/重量级锁/自旋锁;锁升级/锁降级

偏向锁、轻量级锁、重量级锁、自旋锁都是与synchronized有关系的。synchronized在JVM实现上,会根据锁的竞争状况进行锁升级。

偏向锁是JDK1.6提出来的一种锁优化的机制。其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式。

如果偏向锁失败,这时候锁会升级,Java虚拟机就会让线程申请轻量级锁,轻量级锁在虚拟机内部,轻量级锁是采用CAS自旋的方式来获取锁。

当轻量级锁失败,这时候锁会升级,虚拟机就会使用重量级锁。重量级锁在操作过程中,线程可能会被操作系统层面挂起,如果是这样,线程间的切换和调用成本就会大大提高。

锁降级在这个过程中,是不被允许的。因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

6. 锁消除/锁膨胀

锁消除与锁膨胀是JVM动态对锁进行的优化。

锁消除是指,通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界量的引用),而“自作多情”地给自己加上了锁。有可能虚拟机会直接去掉这个锁。

锁膨胀,指的是这种场景:试想有一个循环,循环里面是一些敏感操作,有的人就在循环里面写上了synchronized关键字。这样确实没错不过效率也许会很低,因为其频繁地拿锁释放锁。要知道锁的取得(假如只考虑重量级MutexLock)是需要操作系统调用的,从用户态进入内核态,开销很大。于是针对这种情况也许虚拟机发现了之后会适当扩大加锁的范围(所以叫锁膨胀)以避免频繁的拿锁释放锁的过程。

7. 悲观锁/乐观锁

这个概念指的是具体锁的策略。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。比如独占锁,其实就是一种悲观的策略,总是认为只要不去做正确的同步措施,就会导致同步出错,无论共享数据是否真的会出现竞争,都由某一个线程去获取锁。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。


文章 Java并发小结(二) — 常见的锁分类 转载需要注明出处
喜欢 (0)

您必须 登录 才能发表评论!