Java并发锁面试题(一)

107 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情 >>

悲观锁:synchronized

  • Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。

  • 每个对象头中分为两部分:一部分是自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。一部分是类型指针,即是对象指向它的类的元数据的指针

  • 而对象的锁(monitor)就在对象头中,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。

  • 而其他想要获取该锁只能阻塞,一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,需要消耗很多处理器时间

  • 特性:互斥锁、非公平锁、可重入、不可中断

优点:实现简单

缺点:

  • 不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大

  • 不可中断,在所有等待的线程中,synchronized无法帮你中断此任务

  • 互斥锁在获取锁失败后将进入睡眠或阻塞状态

乐观锁:CAS( compare and swap,比较并交换)

自旋锁

  • 悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。
  • 乐观锁的核心算法是CAS(Compare and Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
  • 这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

优点:高并发性能,jdk中的并发包也大量使用基于CAS的乐观锁。

缺点:

  • 乐观锁只能保证一个共享变量的原子操作
  • 长时间自旋可能导致开销大
  • ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

同步器:AQS(AbstractQueuedSynchronizer)

  • 定义:它为不同场景提供了实现锁及同步机制的基本框架,为同步状态的原子性管理、线程的阻塞、线程的解除阻塞及排队管理提供了一种通用的机制。

  • 原理:ASQ将线程封装到一个Node里面,并维护一个CHL Node FIFO队列,它是一个非阻塞的FIFO队列,也就是说在并发条件下往此队列做插入或移除操作不会阻塞,是通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入。

  • state

  • 独占模式state的值只能为0或1

  • 共享模式的state是可以被出事换成任意整数,一般初始值表示提供一个同时n条线程通过的管道宽度,这样一来,多条线程通过tryAcquireShared尝试将state的值减去1,成功修改state后就返回新值,只有当新值大于等于0才表示获取锁成功,拥有往下执行的权利,进入管道。在执行完毕时线程将调用tryReleaseShared尝试修改state值使之增加1,

  • 表示我已经执行完了并让出管道的通道供后面线程使用,需要说明的是与独占模式不同,由于可能存在多条线程并发释放锁,所以此处必须使用基于CAS算法的修改方法,修改成功后其他线程便可继续竞争锁。

  • 独占式:只容许一个线程通过的管道,在这种模式下线程只能逐一通过管道,任意时刻管内只能存在一条线程,这便形成了互斥效果

  • 共享式:共享模式就是管道宽度大于1的管道,可以同时让n条管道通过,吞吐量增加但可能存在共享数据一致性问题

阻塞唤醒三种方式:

  • suspend与resume:存在无法解决的竟态问题而被Java废弃

  • wait与notify:这两个方法必须存在于synchronized中,存在竟态条件,wait必须在notify之前执行,假如一个线程先执行notify再执行wait将可能导致一个线程永远阻塞

  • await与singal: Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法

wait与await区别:

  • wait与notify必须配合synchronized使用,因为调用之前必须持有锁,wait会立即释放锁,notify则是同步块执行完了才释放
  • 因为Lock没有使用synchronized机制,故无法使用wait方法区操作多线程,所以使用了Condition的await来操作
  • park与unpark:由LockSupport类提供,底层调用的是Unsafe类的方法,由于park与unpark使用的是许可机制,许可最大为1,所以unpark与park操作不会累加,而且unpark可以在park之前执行,如unpark先执行,后面park将不阻塞。
  • Lock实现主要是基于AQS,而AQS实现则是基于LockSupport,所以说LockSupport更底层,所以不建议使用park和unpark去阻塞和唤醒线程