必问系列 | 12张图带你彻底理解Java中的各种锁~
共 4564字,需浏览 10分钟
·
2021-12-15 21:15
前文
大家好,我是小龙。
又和大家见面了~
今天想和大家聊聊关于Java中的各种锁,有幸在秋招面试中经常被问到这块,所以详细整理了一下和大家分享一下,下次面试遇到可以游刃有余~
再看正文之前,大家可以闭眼回想一下这样些个问题。
面试官:“对锁了解吗?谈谈你知道的锁?何为乐观锁?怎样实现?分段锁哪里有体现?这样设计有什么好处?
”
如果你都能很快回忆起来,说明基础还是很扎实的。不过也不要急,可以继续往下看,说不定就有意外收获。
如果你感觉很模糊啦,那这篇文章正适合你,补录进行式,春招即将进行式。秋招没拿到理想offer的同学赶快收藏转发起来。
本篇精华涉猎:
悲观锁与乐观锁
悲观锁
何为悲观?永远处于一个消极悲观状态,因此悲观锁觉得并发操作每次都可能有问题,于是每次都会加锁。
稍微详细解释:
悲观锁
认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式
。悲观的认为,不加锁并发操作一定会出问题。
如图所示,假设现在有多个线程想操作同一个资源对象,可能有人就会想到使用互斥锁进行同步,而它的同步方式就是悲观的。认为如果不严格同步线程调用,便会出问题。因此,互斥锁会将资源锁定,只供一个线程使用。
乐观锁
何为乐观
?永远处于乐观积极状态,因此乐观锁觉得并发操作期间是不会出问题的,操作数据不加锁,只会最后更新数据时检查数据有没有被修改,没有的话才更新。
通常乐观锁可以用CAS算法
或则vision版本机制
实现。在Java中,java.util.concurrent.atomic
包下的原子类就是使用CAS实现的。
既然提到了CAS,这个也是面试高频问题,这里便将 CAS 做个简单介绍,可以将乐观锁和 CAS 二者联系起来理解。
CAS 是一种乐观锁实现机制,主要是三部分:内存值+旧的预期值+要修改的值。每次修改数据先比较内存中值与预期值是否相同,不同就自旋,相同才修改。实现依靠unsafe
(里面全是native修饰的本地方法,可以直接调用操作系统)+lock cmpxchg
(底层依靠硬件指令)。
如图,原本共享变量 old value=0 ,线程修改数据先比较内存中的值是否为0,若为0,代表没有线程占用,此时才修改为 new value=1,当其他线程到达,发现内存值与 old value 不一样了,便自旋等待。
CAS缺点:
可能造成 ABA (version)
问题——当一个值从A被更新为B,然后又改回来,普通 CAS 机制发现不了。一直 while 浪费资源:若并发量高,许多线程反复尝试更新变量又更新不成功,循环往复,会给 CPU 带来高消耗。 不能保证代码块原子性:只能保证一个变量的原子操作,代码块要用 sychronized。
乐观锁与悲观锁的使用场景
这两种锁各有自己优缺点,只有在合适的场景使用合适的锁,才能将各自的优势最大化体现出来。
乐观锁适用于读多写少的场景,因为它是不加锁的,相较于悲观锁不用加锁、释放锁,节省了开销。但是若写多,冲突严重,可能导致线程一直 while 自旋,浪费资源,反而降低了性能。此时在这种写多读少的场景使用悲观锁就更合适。
独占锁和共享锁
独占锁
独享锁
也叫排他锁
,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK 中的 synchronized 和 JUC 中 Lock 的实现类就是互斥锁。
共享锁
共享锁
是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
需要注意的是,对于Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
独占锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。
对于 synchronized 而言,当然是独占锁。
公平锁和非公平锁
公平锁
公平锁
是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
公平锁的优点是等待锁的线程不会饿死
。缺点是整体吞吐效率相对非公平锁要低
,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁
非公平锁
是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高
,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死
,或者等很久才会获得锁。
在Java中 synchronized
是非公平锁,ReentrantLock
可以是非公平锁,也可以是公平锁,默认非公平锁。
//此处创建一个非公平锁,默认就是非公平,true 表示公平,false 表示公平。
Lock lock =new ReentrantLock(flase);
此处既然谈到了 synchronized
和 ReentrantLock
,那么,便顺便与Lock联系起来做个简单的对比。
来源:synchronized 是关键字,lock是一个接口,实现类进行锁操作。 是否知道获取锁:synchronized 无法判断锁的状态,lock 可以判断是否获得锁 (boolean b =lock.tryLock();) 是否释放锁:synchronized 自动释放锁,lock 手动释放(容易死锁) lock.unlock();synchronized 阻塞后,其他线程一直等待,lock有超时时间。 synchronized 可重入锁(多次获取同一把锁),不可中断,非公平锁;lock,默认为非公平锁,可设置为公平。 调度机制:synchronized 使用 Object 对象本身的 wait 、 notify、notifyAll 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度。 是否响应中断:lock 等待锁过程中可以用 interrupt 来中断等待,而 synchronized 只能等待锁的释放,不能响应中断。
分段锁
分段锁
其实是一种锁的设计,并不是具体的一种锁,具体在 ConcurrentHashMap JDK1.7 版本有所体现。其并发的实现就是通过分段锁的形式来实现高效的并发操作
。
何为分段锁?为何引入分段锁?
不妨同 Hashtable 进行比较,我们都知道 Hashtable 是并发安全的,但是它的效率却很低下,因为它将整个表都锁起来了。这样就使得很大程度降低了性能。而 ConcurrentHashMap JDK1.7 引入了分段锁。
一个 Segment 就相当于一把锁,它只锁住这个槽位,其他的并不受影响。ConcurrentHashMap 将 hash 表分为 16 个桶(默认值),诸如get,put,remove 等常用操作只锁当前需要用到的桶。
试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
ConcurrentHashMap JDK1.7 中 数据结构是由 Segment 数组+ HashEntry 数组+链表组成的。
Segment 继承了 ReentrantLock,一个 Segment[i] 就是一把分段锁。比起 Hashtable 锁粒度更细,性能更高。
一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
static final class Segment<K,V> extens ReentrantLock implements Serializable{
transient volatile HashEntry[] tables;
//.....
}
static final class HashEntry<K,V>
{
final int hash;
final K key;
volatile V value;
volatile HashEntry next;
}
可重入锁
可重入锁
又名递归锁
,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
synchronized void getA() throws Exception{
Thread.sleep(1000);
getB();
}
synchronized void getB() throws Exception{
Thread.sleep(1000);
}
对于Java ReetrantLock 而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。对于 Synchronized 而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
此处是我分析 JDK 源码画的 AQS 源码解析流程图
,可以带你很好的理解关于非公平锁
和可重入锁
内部具体实现。此处可能有点模糊,若有需要可以后台回复【AQS】领取。
自旋锁
自旋锁
是线程在没有获取到锁时不会立即阻塞,会一直while循环去尝试获取锁,这个便称为自旋。
自旋可以减少线程被挂起,从而减少线程上下文切换的消耗。
但是若锁被一个线程长时间占用,那么一直循环自旋,便会浪费系统资源,反而降低了整体性能。
小龙有话说
本文以图解方式给大家介绍了Java中的各种锁,这一块面试也特喜欢考,希望大家可以下来再多看看这块相关的知识~
如果喜欢这个图解系列文章的朋友欢迎留言,关注,小龙致力于以最通俗易懂的方式给大家分享知识~
我是小龙,我们下期见。
你要是愿意,我就永远爱你
你要是不愿意,我就永远相思
——《爱你就像爱生命》
求一键三连:希望转发、在看、分享给更多同学哟~
公众号:大厂进阶指南,专注分享后端技术、校招面试求职~
相逢必是缘分,希望大家给个小小的关注!您的支持是我莫大的动力,更多优质好文等您来探索,爱你哟
粉丝福利:后台回复【助力礼包】领取校招求职全套攻略;回复【基于人工智能的智慧校园助手】领取校招求职精品项目。