每天十道面试题-20200408

每天十道面试题-20200408

        • 题目
        • 解答
            • 题目一
            • 题目二
            • 题目三
            • 题目四
            • 题目五
            • 题目六
            • 题目七
            • 题目八
            • 题目九
            • 题目十

题目

  • 1、垃圾回收算法,CMS垃圾回收器简单介绍?
  • 2、UDP,TCP,HTTP介绍一下,OSI七层模型?
  • 3、HashMap?
  • 4、聊聊B树和B+树 数据结构是什么 查询复杂度是多少?
  • 5、介绍HashMap ,与TreeMap区别?
  • 6、分布式锁的实现?
  • 7、分布式锁过期解决?
  • 8、前缀树是什么 前缀树的使用场景?
  • 9、分布式数据库主从复制?
  • 10、MySQL 死锁发生的原因和解决?

解答

题目一
  • 题干:垃圾回收算法,CMS垃圾回收器简单介绍?
  • 分析:
  • 程序运行会产生很多对象占用内存空间所以需要回收并释放空间。
    回收算法:计数算法、复制算法、标记清除算法、标记压缩算法。
    存活少的适用赋值算法,因为复制的对象少,存活多的不适用赋值算法,一般会采取标记清除算法、标记压缩算法。
    分配对象的时候,首先会给每一个线程分配一个TLAB内存区,现在这个地方分配,太大的再去堆上分配,堆上分为三个区域:eden survivor old 首先在eden区分配经过第一次GC之后存活的对象移动到survivor区这里分为from 和to 会来回互换,我们可以设置一个阈值,这个阈值时移动的最大值,不一定一定要达到这个值才移动到老年代,
    年轻代的回收触发机制是:年轻代使用率99%或者接近快用完的时候。
    老年代的回收:
    当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
    CMS垃圾回收器的工作过程与其他的垃圾收集器相比,略显复杂,主要流程:初识标记【STW标记根对象】、并发标记【标记所有对象】、预清理【清理前准备以及控制停顿时间】、重新标记【STW修正并发标记数据】、并发清除【清理垃圾】和并发重置【指在完成后重新初始化CMS数据结构和数据,为下一次垃圾回收做准备】,其中初识标记和重新标记是独占资源系统的,而剩余的步骤是可以和用户线程一起执行的。因为从整体上来说,CMS不是独占式的,它可以在引用程序运行过程中进行垃圾回收,

  • 回答:
  • 见分析。

题目二
  • 题干:UDP,TCP,HTTP介绍一下,OSI七层模型?
  • 分析:
  • 1.物理层:建立、维护、断开物理连接;
    2.数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能;
    3.网络层:进行逻辑地址寻址、实现不同网络之间的路径选择;
    4.传输层:定义传输数据得协议端口,以及流程和差错校验,协议有TCP、UDP,数据包一旦离开网卡即进入网络传输层;
    5.会话层:建立、管理、终止会话;
    6.表示层:数据的表示、安全、压缩;
    7.应用层:网络协议与最终用户得一个接口,协议有http ftp tftp smtp snmp dns telnet https pop3 dhcp。
    一、TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
    二、也正由于1所说的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。知道了TCP和UDP的区别,就不难理解为何采用TCP传输协议的MSN比采用UDP的QQ传输文件慢了,但并不能说QQ的通信是不安全的,因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP因为在底层协议的封装上没有采用类似TCP的“三次握手”而实现了TCP所无法达到的传输效率。
    HTTP
    HTTP属于TCP/IP应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。
    WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
    HTTP协议是建立在请求/响应模型上的。首先由客户建立一条与服务器的TCP链接,并发送一个请求到服务器,请求中包含请求方法、URI、协议版本以及相关的MIME样式的消息。服务器响应一个状态行,包含消息的协议版本、一个成功和失败码以及相关的MIME式样的消息。
    HTTP/1.0为每一次HTTP的请求/响应建立一条新的TCP链接,因此一个包含HTML内容和图片的页面将需要建立多次的短期的TCP链接。一次TCP链接的建立将需要3次握手。但HTTP/1.1里面有所改变,可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。
    由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
    工作特点:
    1.基于B/S模型;
    2.通信开销小,简单快速,传输成本低;
    3.使用灵活,可使用超文本传输协议;
    4.节省传输时间;
    5.无状态;
    工作原理:
    客户端发送请求给服务器,创建一个tcp连接,指定端口号,默认80,连接到服务器,服务器监听到浏览器请求,一旦听到客户端请求,分析请求类型后,服务器会向客户端返回状态信息和数据内容;。

  • 回答:
  • 见分析。

题目三
  • 题干:HashMap?
  • 分析:
  • 参考链接。

  • 回答:
  • 见分析。

题目四
  • 题干:聊聊B树和B+树 数据结构是什么 查询复杂度是多少?
  • 分析:
  • 平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构。
    平衡树的层级结构:因为平衡二叉树查询性能和树的层级(h高度)成反比,h值越小查询越快、为了保证树的结构左右两端数据大致平衡降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树,使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找;
    平衡二叉树 深度越深查找性能越差送一尽量让其平衡且深度不深。
    B树就是平衡多路查找树【查找路径不止两个】
    B+ 树的结构是一颗N阶查找树
    1、B树的非叶子结点 是存储数据的指针的所以每一个非叶子结点占用的存储更多,也就导致B树的非叶子节点更多深度更深。而B+ 树不保存指向数据的指针,只进行索引,也就是只做目录查找,所以可以存储更多的非叶子结点,这样同样的存储非叶子结点。
    2、B+树非叶子结点都是目录页,所有的数据都存放在叶子节点,按照主键从小到大。由于数据在叶子结点所以每次查找复杂度一样。
    B+树接近与二叉树的复杂度O(Log2n)到O(n)之间

  • 回答:
  • 见分析。

题目五
  • 题干:介绍HashMap ,与TreeMap区别?
  • 分析:
  • 首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.这就是我们平时说的键值对。
    HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
    HashMap 代码分析
    HashMap 是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
    官方文档如下:
    此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
    HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
    通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
    如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
    注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
    Map m = Collections.synchronizedMap(new HashMap(…));
    由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
    Map.put()的时候,需要将key值映射为相应的hash值,key的值是以char数组的形式存放的,value对应的值也是有char数组存放的
    在进行存放的时候,首先检查table是否为空,如果为空使用inflateTable方法进行初始化操作。
    这里用到的hash方法如下:
    Map.get()的时候,是根据hash值进行查找的:
    然后就是调用hash方法,找到具体的key所对应的hash,然后再到entry中去找value
    TreeMap代码分析
    看到上边,可知TreeMap并不是基于hash实现的,据说是红黑树,红黑树这块几乎空白,不敢多说:
    TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
      (1)TreeMap():构建一个空的映像树
      (2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素
      (3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序
      (4)TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序。
    官方文档:
    基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
    此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。这些算法是 Cormen、Leiserson 和 Rivest 的 Introduction to Algorithms 中的算法的改编。
    注意,如果要正确实现 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须与 equals 一致。(关于与 equals 一致 的精确定义,请参阅 Comparable 或 Comparator)。这是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的 compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使排序与 equals 不一致,有序映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。
    注意,此实现不是同步的。如果多个线程同时访问一个映射,并且其中至少一个线程从结构上修改了该映射,则其必须 外部同步。(结构上的修改是指添加或删除一个或多个映射关系的操作;仅改变与现有键关联的值不是结构上的修改。)这一般是通过对自然封装该映射的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSortedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的不同步访问,如下所示:
    SortedMap m = Collections.synchronizedSortedMap(new TreeMap(…));
    collection(由此类所有的“collection 视图方法”返回)的 iterator 方法返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 方法,否则在其他任何时间以任何方式进行修改都将导致迭代器抛出 ConcurrentModificationException。因此,对于并发的修改,迭代器很快就完全失败,而不会冒着在将来不确定的时间发生不确定行为的风险。
    HashMap和TreeMap比较
    (1)HashMap:适用于在Map中插入、删除和定位元素。
    (2)Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
    (3)HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap.
    (4)HashMap 非线程安全 TreeMap 非线程安全
    (5)HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的。
     在HashMap中通过get()来获取value,通过put()来插入value,ContainsKey()则用来检验对象是否已经存在。可以看出,和ArrayList的操作相比,HashMap除了通过key索引其内容之外,别的方面差异并不大。

  • 回答:
  • 见分析。

题目六
  • 题干:分布式锁的实现?
  • 分析:
  • 1、zookeeper
    使用zookeeper实现分布式锁,由于zk具有天然的锁性质,所以可以用其实现,而且稳定可靠。
    一般场景比如smowflake实现的自增ID ,这是一种在分布式环境中生成全局唯一ID的算法,
    特点:按照时间非连续递增,分布式环境下不会出现碰撞、快。
    缺点:只能趋势递增。
    依赖机器时间,如果发生回拨会导致可能生成id重复。
    算法实现大概是这样:
    其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
    1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
    41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的
    10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId。10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
    12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
    加起来刚好64位,为一个Long型。
    我们可以自己调整workID和dataCenterID的占用大小
    就是缩小前面41bit的时间戳,拿出一部分给10bit的数据区
    snowflake这里又用到枚举实现的单例,通过这个单例去获取SnowflakeWorkr方法中 定义的nextId来生成主键,这个方法是一个同步方法。
    枚举法实现的单例:一、反编译后是静态的成员变量,根据类加载机制是只初始化一次,二、可以解决反序列化破坏单例,因为反序列化的时候使用writeObject会重新new一个单例对象出来,而jdk源码对于枚举,禁用了readObject和writeObject,通过序列化输出name反序列化调用valueof方法实现保证单例。
    2、Redis
    Redis做不到决定的锁定,想较于zk差很多。
    Redis并不能实现严格意义上的分布式锁。但是这并不意味着上面讨论的方案一无是处。如果你的应用场景为了效率(efficiency),协调各个客户端避免做重复的工作,即使锁失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。但是如果你的应用场景是为了正确性(correctness),那么用Redis实现分布式锁并不合适,会存在各种各样的问题,且解决起来就很复杂,为了正确性,需要使用zab、raft共识算法,或者使用带有事务的数据库来实现严格意义上的分布式锁。
    使用redis实现分布式锁分布式锁需要解决的问题:
    互斥性:任意时间只能有一个客户端获取锁。不能同时有两个客户端获取锁。
    安全性:锁只能由持有锁的客户端删除,不能由其他的客户端删除。
    死锁:获取锁的客户端因为某些原因宕机了,而未能释放锁,其他客户端再也无法获取到该锁。导致的死锁。
    容错:当一些redis节点宕机,客户端仍能获取锁和释放锁。
    SETNX key value :如果key不存在,则创建并赋值。(时间复杂度是O(1)设置成功返回1 设置失败返回0)
    解决setnx长期有效的问题,使用EXPIRE key seconds
    设置key的生存时间,当key过期时(生存时间为0),会被自动删除。
    但是这种方式原子性得不到满足,虽然每一个单独的操作是原子性的但是两个操作合在一起就不满足了,比如setnx后执行expire失败了 这时就会出现问题了。
    redis 2.6.12开始我们就可以将这两个操作融合再一次成为一个原子性操作。
    SET key value 【EX seconds】【PX milliseconds】【NX】【XX】
    EX seconds 设置键的过期时间为second 秒
    PX milliseconds 设置键的过期时间为millisecond 毫秒。
    NX 只在键不存在时 才对键进行设置操作。
    XX 只在键已经存在时 才对键进行设置操作。
    Set 操作完成时 返回OK 否则返回 nil。
    Java 中Jedis实现分布式锁的方式如下图:
    String cacheKey = CommonUtil.getCacheKey(false, aaa, bbb);
    if(redisClient.set(cacheKey, JSONObject.toJSONString(resultMap), NX.NOTEXIST, TimeUnit.SECOND, AsyncService.CACHE_TIME)){
    }
    出现大量key同时过期的注意事项。集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象。
    解决方案:在设置key的过期时间的时候,给每个key加上随机值。
    3、基于数据库实现的分布式锁
    For update ,第二种就是使用乐观锁。

  • 回答:
  • 见分析。

题目七
  • 题干:分布式锁过期解决?
  • 分析:
  • 1.获取当前时间。
    2.按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。
    3.计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
    4.如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
    5.如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的单机Redis Lua脚本释放锁的方法)。
    延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

  • 回答:
  • 见分析。

题目八
  • 题干:前缀树是什么 前缀树的使用场景?
  • 分析:
  • 利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
    假设字符的种数有m个,有若干个长度为n的字符串构成了一个 Trie树 ,则每个节点的出度为 m(即每个节点的可能子节点数量为m),Trie树 的高度为n。很明显我们浪费了大量的空间来存储字符,此时Trie树的最坏空间复杂度为O(m^n)。也正由于每个节点的出度为m,所以我们能够沿着树的一个个分支高效的向下逐个字符的查询,而不是遍历所有的字符串来查询,此时Trie树的最坏时间复杂度为O(n)。
    这正是空间换时间的体现,也是利用公共前缀降低查询时间开销的体现。
    利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
    假设字符的种数有m个,有若干个长度为n的字符串构成了一个 Trie树 ,则每个节点的出度为 m(即每个节点的可能子节点数量为m),Trie树 的高度为n。很明显我们浪费了大量的空间来存储字符,此时Trie树的最坏空间复杂度为O(m^n)。也正由于每个节点的出度为m,所以我们能够沿着树的一个个分支高效的向下逐个字符的查询,而不是遍历所有的字符串来查询,此时Trie树的最坏时间复杂度为O(n)。
    这正是空间换时间的体现,也是利用公共前缀降低查询时间开销的体现。

  • 回答:
  • 见分析。

题目九
  • 题干:分布式数据库主从复制?
  • 分析:
  • 1.一台服务器开多个Mysql服务
    2.主数据库配置
    3.从数据库配置
    4.主从数据库同步
    这里要强调一下主从数据库架构是用来实现高可用的而不是来解决高并发问题的,如果我们能接受延迟问题,对于主从数据库可以使用完全同步的模式即主从数据库架构的强一致性,如果不接受,那么这种主库进行写从库进行读的模式,一般建议使用redis缓存来缓解高并发压力。
    参考第三题

  • 回答:
  • 见分析。

题目十
  • 题干:MySQL 死锁发生的原因和解决?
  • 分析:
  • 1、死锁产生原因:
    所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。
    死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
    那么对应的解决死锁问题的关键就是:让不同的session加锁有次序
    2、解决:
    查看死锁日志,找到等待获取锁的语句。然后根据语句以及业务进行分析。

  • 回答:
  • 见分析。

你可能感兴趣的:(【面试题】,java)