[java] 2024--今日头条面试题及参考答案
前言
java今日头条面试题.pdf
链接:https://pan.quark.cn/s/cbb060a021a1
提取码:Xx1W
2024 Java面试宝典合集
链接:https://pan.quark.cn/s/3b5f78597c32
提取码:CHN1
MySQL 锁概述
相对其他数据库而言,MySQL 的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。
比如:
. MyISAM 和 MEMORY 存储引擎采用的是表级锁(table-level locking);
. InnoDB 存储引擎既支持行级锁( row-level locking),也支持表级锁,
但默认情况下是采用行级锁。
MySQL 主要的两种锁的特性可大致归纳如下:
表级锁: 开销小,加锁快;不会出现死锁(因为 MyISAM 会一次性获得 SQL 所需的全部锁);锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的 概率最低,并发度也最高。
页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
行锁 和 表锁
1.主要是针对锁粒度划分的,一般分为:行锁、表锁、库锁
(1)行锁:访问数据库的时候,锁定整个行数据,防止并发错误。
(2)表锁:访问数据库的时候,锁定整个表数据,防止并发错误。
2.行锁 和 表锁 的区别:
表锁: 开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
行锁: 开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
悲观锁 和 乐观锁
(1)悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
(2)乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition 机制的其实都是提供的乐观锁。
(3)悲观锁 和 乐观锁的区别:
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行 retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
共享锁
共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。相当于对于同一把门,它拥有多个钥匙一样。就像这样,你家有一个大门,大门的钥匙有好几把,你有一把,你女朋友有一把,你们都可能通过这把钥匙进入你们家,这个就是所谓的共享锁。刚刚说了,对于悲观锁,一般数据库已经实现了,共享锁也属于悲观锁的一种,那么共享锁在 mysql 中是通过什么命令来调用呢。通过查询资料,了解到通过在执行语句后面加上 lock in share mode 就代表对某些资源加上共享锁了。什么时候使用表锁对于 InnoDB 表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择 InnoDB 表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然,应用中这两种事务不能太多,否则,就应该考虑使用 MyISAM 表了。
表锁和行锁应用场景:
表级锁使用与并发性不高,以查询为主,少量更新的应用,比如小型的web 应用; 而行级锁适用于高并发环境下,对事务完整性要求较高的系统,如在线事务处理系统。
乐观锁 VS 悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在 Java和数据库中都有此概念对应的实际应用。
1.乐观锁
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的。
CAS 全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过 CAS 来实现了乐观锁。
简单来说,CAS 算法有 3 个三个操作数:
需要读写的内存值 V
进行比较的值 A
要写入的新值 B
当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则返回 V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized 是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。
2.悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的 MySQL 关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。详情可以参考:阿里 P8 架构师谈: MySQL 行锁、表锁、悲观锁、乐观锁的特点与应用
再比如上面提到的 Java 的同步 synchronized 关键字的实现就是典型的悲观锁。
3.总之:
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
公平锁 VS 非公平锁
1.公平锁
就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
2.非公平锁
上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
3.典型应用:
java jdk 并发包中的 ReentrantLock 可以指定构造函数的 boolean 类型来创建公平锁和非公平锁(默认),比如:公平锁可以使用 new ReentrantLock(true)实现。
独享锁 VS 共享锁
1.独享锁
是指该锁一次只能被一个线程所持有。
2.共享锁
是指该锁可被多个线程所持有。
3.比较
对于 Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
3.典型应用:
java jdk 并发包中的 ReentrantLock 可以指定构造函数的 boolean 类型来创建公平锁和非公平锁(默认),比如:公平锁可以使用 new ReentrantLock(true)实现。
独享锁 VS 共享锁
1.独享锁
是指该锁一次只能被一个线程所持有。
2.共享锁
是指该锁可被多个线程所持有。
3.比较
对于 Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。
4.AQS
抽象队列同步器(AbstractQueuedSynchronizer,简称 AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的 volatile 变量(命名为 state) 来维护同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作
concurrent 包的实现结构如上图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于 volatile 变量的读/写和 CAS 实现,而像 Lock、同步器、阻塞队列、Executor 和并发容器等高层类又是基于基础类实现。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以 ConcurrentHashMap 来说一下分段锁的含义以及设计思想,ConcurrentHashMap 中的分段锁称为 Segment,它即类似于 HashMap(JDK7 与 JDK8中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计。 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅 针对数组中的一项进行加锁操作。
什么场景需要 JVM 调优
. OutOfMemoryError,内存不足
. 内存泄露
. 线程死锁
. 锁争用(Lock Contention)
. Java 进程消耗 CPU 过高
这些问题出现的时候常常通过重启服务器或者调大内存来临时解决,实际情况,还需要尽量还原当时的业务场景,并分析内存、线程等数据,通过分析找到最终的解决方案,这就会涉及到性能分析工具