简介
锁,是并发场景主流的控制手段,不同的场景适用的锁不同,比如有的场景并发率低,适用乐观锁;有的场景并发率高,适用悲观锁。
甚至可以将锁粒度进一步细分,比如读写锁,一般系统大部分的操作是读操作,因此,将读写操作分离,分别控制,将大大提升系统的吞吐率。
锁,本质是要让并发场景下,对同一对象/记录的修改操作排队串行处理,对于性能来讲,这必然是一个瓶颈;因此,锁的发展也是围绕着如何高效的解决这个瓶颈问题,“两害相权取其轻”,不同的业务场景,适用的锁也不同,没有一把锁可以在任何场景下都适用!
锁分类
乐观锁和悲观锁
1、悲观锁:
悲观锁(Pessimistic Lock), 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。这样其他人想拿数据就被挡住,直到悲观锁被释放。
悲观锁代表有:synchronized
2、乐观锁:
乐观锁(Optimistic Lock), 每次去拿数据的时候都认为别人不会修改。所以不会上锁! 但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。
如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的。
乐观锁代表有:AtomicInteger
CAS: 全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁的情况下实现多线程之间的变量同步。
CAS算法涉及到三个操作数:
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS 强调的是一个整体,要么同时成功、要么同时失败。
CAS存在的问题:
自旋锁 VS 适应性自旋锁
核心考量是:适当自旋的开销 VS 线程上下文切换的开销
1、自旋锁:
阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程稍等一下,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
通过自旋锁减少 CPU 切换以及恢复现场导致的消耗。
2、自适应自旋锁:
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁是指锁的状态,专门针对 synchronized 的特性。
正确理解对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
对象头由以下三部分组成:
其中,Mark Word 在 64 位 JVM 的存储:
JVM 是如何使用 Mark Word?
公平锁 VS 非公平锁
在 Java 的 JUC 并发包下有 AQS 的实现,其中就有公平锁和非公平锁的实现。
1、公平锁:
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
2、非公平锁:
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
比如,Java 的 ReentrantLock 在构造时可以指定公平还是非公平锁,默认为非公平锁。
可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
独享锁 VS 共享锁
1、独享锁:
也叫排他锁、独占锁,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。
获得排它锁的线程既能读数据又能修改数据。JDK 中的 synchronized 和 JUC 中 Lock 的实现类就是互斥锁。
2、共享锁:
是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
小结
本文主要讲解了 Java 主流锁的特性,每个特性都有其优劣势,因此在当前主流的锁实现中,通常都会组合几个特性进行实现,比如 synchronized 包含特性:
当然,融合特性越多,锁越偏重量级,因此有实现特性少的,如 AtomicXxx 系列支持原子类操作,主要采用 CAS 乐观锁去实现原子特性。
在多线程编程中,选择合适的锁机制是非常重要的,因为它直接影响到程序的性能和正确性,可以结合以上特性分析,选择最合适场景的锁进行开发。
作者:柏油
链接:
https://juejin.cn/post/7379884568578555931
#头条创作挑战赛#