必问系列 | 12张图带你彻底理解Java中的各种锁~

JavaEdge

共 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);

此处既然谈到了 synchronizedReentrantLock,那么,便顺便与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,Vextens 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】领取。

AQS原理解析

自旋锁

自旋锁 是线程在没有获取到锁时不会立即阻塞,会一直while循环去尝试获取锁,这个便称为自旋。

小龙手绘:自旋锁

自旋可以减少线程被挂起,从而减少线程上下文切换的消耗。

但是若锁被一个线程长时间占用,那么一直循环自旋,便会浪费系统资源,反而降低了整体性能。

小龙有话说

本文以图解方式给大家介绍了Java中的各种锁,这一块面试也特喜欢考,希望大家可以下来再多看看这块相关的知识~

如果喜欢这个图解系列文章的朋友欢迎留言关注,小龙致力于以最通俗易懂的方式给大家分享知识~

我是小龙,我们下期见。

你要是愿意,我就永远爱你


你要是不愿意,我就永远相思


——《爱你就像爱生命》



往期精彩回顾




系统后台服务变慢,怎样排查诊断?
字节面试官:一条sql执行慢的原因?如何优化?
《面试笔记》——JVM终结篇(吊打系列)
《面试笔记》——MySQL终结篇(30问与答)


---END---

求一键三连希望转发在看分享给更多同学哟~

公众号:大厂进阶指南,专注分享后端技术、校招面试求职~

相逢必是缘分,希望大家给个小小的关注!您的支持是我莫大的动力,更多优质好文等您来探索,爱你哟

粉丝福利后台回复【助力礼包】领取校招求职全套攻略;回复【基于人工智能的智慧校园助手】领取校招求职精品项目。

写留言

浏览 32
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报