得物二面,题型很典型

题目来源:来源忘记了,写完解析忘记贴地址了

前文

本期是【捞捞面经】系列文章的第 3 期,持续更新中.....。

捞捞面经》系列正式开始连载啦,据说看了这个系列的朋友都拿到了大厂offer~

捞捞面经

注:养成先看真题,自己模拟回答,再看解析参考(别忘随手一键三连哦~)

基础题:

1.Java 中类加载过程?

2.HashMap 和 HashSet 的区别

3.有没有遇到过死锁?怎么解决的?

4.Java 中常见的锁有哪些,其特点简单说说?

5.乐观锁了解吗?有哪些实现方式?

6.悲观锁有哪些?有哪些实现方式?

7.谈谈常见的设计模式?

8.代理模式有哪两种?动态代理有哪两种?

9.MySQL 隔离级别有哪些?

10.RR隔离级别幻读解决了吗?怎么解决幻读?

11.操作系补码是啥,为啥这么做?

情景题:

12.单词频率统计怎么做,怎么优化?

本文解析收录:https://github.com/xlcoding/NowcoderTop

1. Java中类加载过程

Java中的类加载过程主要包括以下几个步骤:

  1. 加载(Loading):加载阶段主要完成以下三个动作:
  • 通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的数据访问入口。
  1. 验证(Verification):验证阶段主要包括四个检验过程:
  • 文件格式验证:检验导入类数据流是否符合Class文件格式规范。元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。字节码验证:通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。符号引用验证:确保解析动作能正确执行。
  1. 准备(Preparation):为类变量(即被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行分配。
  2. 解析(Resolution):将常量池内的符号引用替换为直接引用的过程。符号引用就是一组符号来描述所引用的目标,直接引用就是直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。
  3. 初始化(Initialization):执行类构造器<clinit>()方法的过程。这个方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

2. HashMap 和 HashSet的区别

HashMap和 HashSet 是Java集合框架中的两种不同类型的集合,它们的主要区别在于存储的数据和使用的场景

  1. HashMap 是一个实现了 Map 接口的类,它存储的是键值对(Key-Value)结构的数据,每个键都是唯一的,用于快速查找值。HashMap 允许键和值为 null。
  2. HashSet 是一个实现了 Set 接口的类,它存储的是无序、不重复的元素集合。HashSet 的内部实际上是一个HashMap,它只使用HashMap的键来实现数据存储,由于键不能重复,所以 HashSet 中的元素也就不会重复。

在使用上,如果需要存储一对键值对,并且需要根据键快速查找值,那么应该使用HashMap。如果需要存储一组元素,但不关心元素的顺序,只需保证元素不重复,那么应该使用HashSet。

3. 有没有遇到过死锁?怎么解决的?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,它们都将无法推进下去。

举个例子,比如有两个线程 A 和 B,线程 A 持有资源1并等待资源 2,而线程B持有资源 2 并等待资源 1,这就形成了一个循环等待,导致死锁。

在实际平时业务场景开发中,通常就是程序死锁,或者数据库 MySQL 死锁之类的。

结合具体场景分析:

1)假设我们有一个在线购物网站,用户可以将商品添加到购物车,然后进行结账。这个过程涉及到两个资源:商品库存和用户的购物车

现在假设有两个用户 A 和 B,他们同时想购买同一件商品。用户 A 已经将商品添加到购物车,正在等待从库存中扣除。与此同时,用户B也将同一件商品添加到购物车,也在等待从库存中扣除。这时,用户A的购物车等待用户 B 释放库存,而用户B的购物车等待用户A释放库存,形成了死锁。

为了避免这种情况,我们可以采取以下策略:

  1. 设置超时退出:如果用户在一定时间内没有完成支付,那么系统自动将商品从购物车中移除,释放库存。
  2. 使用锁顺序:我们可以规定,用户必须先获取商品库存,然后再添加到购物车。这样,所有用户都按照同一顺序获取资源,就不会形成死锁。
  3. 死锁检测与恢复:系统定期检测是否存在死锁,如果检测到死锁,可以选择取消一个用户的操作,让其他用户继续。

2)在MySQL中,死锁主要发生在并发事务中,当两个或多个事务同时锁定了对方需要的资源时,就会发生死锁。

举个例子,假设我们有一个银行转账的业务场景,涉及到两个表:账户表(account) 和 交易记录表(transaction)。在一个转账事务中,我们需要先在账户表中减少转出账户的余额,然后增加转入账户的余额,最后在交易记录表中添加一条记录。

现在假设有两个并发的转账事务A和B,事务A先锁定了账户表,准备更新转出账户的余额,而事务B先锁定了交易记录表,准备添加一条记录。这时,事务A需要锁定交易记录表才能继续,而事务B需要锁定账户表才能继续,就形成了死锁。

为了避免这种情况,我们可以采取以下策略:

  1. 保证事务的原子性:在事务中,所有的操作要么全部成功,要么全部失败。这样可以保证在任何时候,数据都是一致的。
  2. 使用锁顺序:我们可以规定,所有的事务都必须按照同一顺序锁定资源。比如,先锁定账户表,然后锁定交易记录表。这样,就不会有事务在等待其他事务释放资源。
  3. 设置超时退出:如果一个事务在一定时间内没有获取到所有需要的锁,那么就回滚事务,释放已经获取的锁。
  4. 死锁检测与恢复:MySQL可以自动检测死锁,并自动回滚其中一个事务,以解除死锁。

4. Java中常见的锁有哪些,其特点简单说说?

Java中的锁主要有以下几种:

  1. Synchronized:Synchronized是Java中最基本的同步互斥机制,它可以保证同一时刻只有一个线程可以访问被synchronized修饰的代码块或方法。Synchronized在JVM层面实现,属于重量级锁。
  2. ReentrantLock:ReentrantLock是Java并发包java.util.concurrent.locks中的一个类,它提供了与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。ReentrantLock被称为可重入锁,是因为同一个线程可以多次获取同一个锁。
  3. ReadWriteLock:ReadWriteLock是一个读写锁接口,它维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时持有,写入锁是独占的。
  4. StampedLock:StampedLock 是 Java8 引入的一种新的所机制,可以看做是读写锁的一个改进版本,它通过乐观读来解决读写锁的"写饥饿"问题。
  5. SpinLock(自旋锁):自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
  6. CountDownLatch/CyclicBarrier/Semaphore:这些并不是锁,但是它们提供的机制在某些情况下可以实现类似锁的效果,用于控制并发线程的执行。

可以看出它们各有各的特点和使用场景,因此需要根据具体的需求来选择使用哪种锁。

5、乐观锁了解吗?有哪些实现方式?

乐观锁认为数据在一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。

乐观锁的实现一般有两种方式:

  1. 版本号机制:在数据表中加入一个版本号字段,每次读取数据时,将版本号一并读出,数据每更新一次,对应的版本号加1。当我们提交更新的时候,判断数据库表对应记录的当前版本号与我们读出来的版本号进行比对,如果数据库表当前版本号与我们读出来的版本号相等,则予以更新,否则认为是过期数据。
  2. CAS算法:CAS(Compare And Swap)算法涉及到三个操作数,分别是内存值V、预期原值A、新值B。当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。CAS是一种无锁算法,可以避免多线程的互斥等待,提高系统的并发性。

6、悲观锁有哪些?有哪些实现方式?

悲观锁和乐观锁恰好相反,它假设最坏的情况,认为数据在并发处理过程中一定会发生修改,所以在数据处理前就会先加锁,确保数据处理的过程中不会被其他线程修改。

悲观锁的实现方式主要有以下几种:

  1. 数据库锁:数据库本身就提供了一些锁机制,包括行锁、表锁、页锁等,可以在查询语句后加 FOR UPDATE来实现悲观锁。
  2. 共享锁和排他锁:共享锁(S锁)是允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。排他锁(X锁)是允许获取排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
  3. Java中的synchronized和ReentrantLock:另外可以使用 synchronized 关键字或ReentrantLock 来实现悲观锁,保证同一时刻只有一个线程可以执行某个方法或代码块。

6. 谈谈常见的设计模式?

7. 代理模式有哪两种?动态代理有哪两种?

代理模式主要有两种:静态代理和动态代理。

  1. 静态代理:在程序运行前,就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
  2. 动态代理:在程序运行时,运用反射机制动态创建而成。

动态代理又分为两种:JDK动态代理和CGLIB动态代理。

  1. JDK动态代理:其代理对象必须是某个接口的实现,它是通过反射机制来生成一个实现代理接口的匿名类,再通过代理方法回调委托类的方法。这种方式的优点是可以在不修改源码的情况下对委托类的方法进行增强。
  2. CGLIB动态代理:针对类来实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。CGLIB(Code Generation Library),是一个开源项目,是一个强大的、高性能的代码生成包,它广泛被许多AOP的框架使用,例如Spring AOP和synaop等,为他们提供方法的interception(拦截)。

总的来说,代理模式主要用于在不改变原有代码的基础上,增强方法的功能,常用于日志记录,权限控制,事务处理等。

8. MySQL隔离级别有哪些?

MySQL支持以下四种事务隔离级别:

  1. 读未提交(Read Uncommitted):最低的隔离级别,允许读取尚未提交的数据变更,可能导致脏读、幻读或不可重复读。
  2. 读已提交(Read Committed):大多数数据库系统的默认隔离级别(但不包括MySQL和PostgreSQL)。只能读取已经提交的数据。可以防止脏读,但幻读和不可重复读仍可能发生。
  3. 可重复读(Repeatable Read):MySQL的默认隔离级别。在同一事务内的查询都会基于一个快照,保证在同一事务内多次读取同样记录的结果是一致的。但是理论上,该级别仍然无法防止幻读,尽管在InnoDB存储引擎中已经做了处理,实际上可以防止幻读。
  4. 串行化(Serializable):最高的隔离级别,完全服从ACID的隔离级别。所有的事务将依次逐个执行,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响性能,一般不会在实际应用中使用。

9.RR隔离级别幻读解决了吗?怎么解决幻读?

幻读:是指在一个事务内读取到了别的事务插入的数据,导致前后读取的数据不一致。例如,你在一个事务中先后两次执行了相同的查询语句,第一次查询返回了5条记录,但第二次查询却返回了6条记录,这就是幻读。

  1. 在标准的 SQL 隔离级别定义中,可重复读隔离级别并不能解决幻读问题。因为标准定义下的重复读隔离级别,虽然可以防止同一条记录被其他事务修改,但无法防止其他事务插入新的满足查询条件的记录,因此可能出现幻读。只有在串行化(Serializable)隔离级别下,才能完全防止幻读。
  2. 但在 MySQL 的 InnoDB 存储引擎中,可重复读(Repeatable Read)隔离级别实际上已经解决了幻读的问题。
  3. 对于读操作,由于使用了一致性读(consistent read)和多版本并发控制(MVCC),可以保证在同一事务中多次读取同样记录的结果是一致的,避免了不可重复读和幻读的问题。对于写操作,InnoDB 使用了 next-key locking 机制,这种机制会锁定记录行以及索引记录的“间隙”,防止其他事务在这些“间隙”中插入新的记录,从而避免了幻读。

但是,虽然在可重复读隔离级别下,InnoDB 可以有效地防止幻读,但如果事务在读取数据后又进行了写操作(例如,先SELECT,然后UPDATE),可能会出现因为并发操作而导致的数据不一致问题。这种情况下,可能需要使用更高的隔离级别(如串行化)或者显式地使用锁(如SELECT FOR UPDATE)来确保数据的一致性。

10. 操作系统补码是啥,为什么这么做?

补码是一种二进制数的表示形式,主要用于表示负数。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理,即CPU只需要做加法运算即可。

补码的计算方法是:正数的补码就是其本身;负数的补码是在其原码的基础上,符号位不变,其余各位取反(得到反码),然后整个数加1。

例如,假设我们有一个8位的二进制数,+7的原码和补码都是00000111,而-7的原码是10000111,其反码是11111000,再加1得到补码11111001。

使用补码不仅解决了0的符号以及存在两个编码的问题(+0和-0),而且还使得负数的编码更为简单。在使用补码的情况下,减法可以转换为加法,大大简化了计算机硬件的设计。

情景题:

1. 单词频率统计怎么做,怎么优化?

这个问题得看你数据量有多大咯。。。

数据量:几十万或者几万个单词

如果只有几十万或者几万个单词,那么在一台普通的计算机上就可以很好地处理这个问题了。这种情况下,我们可以使用以下的方法:

  1. 使用哈希表或字典:哈希表或者字典可以用来统计每个单词的频率。每次遇到一个单词,都可以在O(1)的时间内找到它的频率并更新它。
  2. 排序并统计:如果需要找出频率最高的N个单词,可以在统计完所有单词的频率后,对单词按照频率进行排序,然后选出前N个。这个步骤的时间复杂度是O(n log n),其中n是单词的总数。

在这种情况下,由于数据量相对较小,所以IO速度和存储空间通常不会成为问题。因此,我们主要需要关注的是算法的效率。使用哈希表或字典可以保证统计频率的步骤非常快,而排序则是一个相对较慢的操作,但由于n不大,所以总的时间复杂度仍然是可以接受的。

数据量:几百万个

如果只有几百万个单词,那么我们可以直接在单台机器上进行处理,无需使用MapReduce等分布式处理框架。以下是具体的步骤:

  1. 使用哈希表或字典:同上。
  2. 使用堆或优先队列:如果需要找出频率最高的N个单词,可以使用堆或者优先队列来存储当前频率最高的N个单词。每次遇到一个新的单词,就和堆顶的单词比较,如果新的单词的频率更高,就将堆顶的单词替换为新的单词。

数据量:几十亿个

对于几十亿个单词的频率统计,可以采取以下步骤:

  1. 使用MapReduce模型:首先,可以将数据分割成多个小块,然后使用多台机器并行处理这些小块数据。这就是MapReduce模型的基本思想。在Map阶段,每台机器处理自己的数据块,并统计每个单词的频率。在Reduce阶段,将所有机器的结果合并,得到每个单词的总频率。
  2. 使用哈希表或者字典:在每台机器上,可以使用哈希表或者字典来统计单词的频率(同上)
  3. 使用堆或者优先队列:同上

那有什么什么好的办法可以优化呢?

优化方面,可以考虑以下几点:

  1. 使用更快的IO方式:如果数据是存储在硬盘上的,那么读取数据的速度可能会成为瓶颈。可以考虑使用更快的IO方式,例如使用SSD硬盘,或者使用内存映射文件。
  2. 使用压缩存储:如果内存空间不足,可以考虑使用压缩存储。例如,可以使用Trie树来存储单词,这样可以大大减少存储空间。
  3. 使用更好的哈希函数:哈希冲突会影响哈希表的性能。可以考虑使用更好的哈希函数,以减少哈希冲突。
  4. 使用多线程或者多进程:如果CPU资源充足,可以考虑使用多线程或者多进程来并行处理数据,以提高处理速度。
#晒一晒我的offer##发现了面试通关密码##java##牛客在线求职答疑中心#
全部评论

相关推荐

8 35 评论
分享
牛客网
牛客企业服务