大家好,我是二哥呀。
一大早醒来,就有球友发来喜讯,说拼多多发录用意向书了,先是感谢了二哥的面渣逆袭和 PmHub,然后我也趁机给大家求了一份经验贴(🤣)。
球友是转正 oc,他给我的反馈是:“之前去另外一个大厂实习过,感受最深的就是,这年头,给钱多才是真的香,去哪都逃不过卷。”
要我说也是,能 work-life-balance 的公司寥寥无几,与其这样,还不如事多钱多加班多。
还没有意向的小伙伴快来吸吸好运 buff。这国庆假期一结束,下一次长假就到春节了,这期间就是销售冲业绩、25 届同学冲 offer 的阶段。
头部和肩部的秋招选手基本上 offer 都拿到手了,这时候就是静等开奖,星球里也有一些球友拿到了多个 offer,来问二哥该如何选择。
早一点拿到 offer 的好处就是可以早点开摆,因为努力冲刺了这么长时间,是时候给自己一些“休养生息”的时间,我只能说,不背八股、不刷 LeetCode、不敲代码的感觉是真的好。
讲良心话,能去拼多多真的已经很厉害了,起跑线肯定是赢了一大批人。那今天我们就以《Java 面试指南》中收录的《拼多多面经同学 4》技术一面为例,来看看拼多多面试官都喜欢问哪些问题,向往大厂的小伙伴好做到知彼知己百战不殆。
让天下所有的面渣都能逆袭 😁
2、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
拼多多同学 4一面面经
对象什么时候进入老年代
对象通常会先在年轻代中分配,然后随着时间的推移和垃圾收集的处理,某些满足条件的对象会进入到老年代中。
二哥的 Java 进阶之路:对象进入老年代
①、
长期存活的对象将进入老年代
对象在年轻代中存活足够长的时间(即经过足够多的垃圾回收周期)后,会晋升到老年代。
每次 GC 未被回收的对象,其年龄会增加。当对象的年龄超过一个特定阈值(默认通常是 15),它就会被移动到老年代。这个年龄阈值可以通过 JVM 参数
-XX:MaxTenuringThreshold
来设置。
②、
大对象直接进入老年代
为了避免在年轻代中频繁复制大对象,JVM 提供了一种策略,允许大对象直接在老年代中分配。
这些是所谓的“大对象”,其大小超过了预设的阈值(由 JVM 参数
-XX:PretenureSizeThreshold
控制)。直接在老年代分配可以减少在年轻代和老年代之间的数据复制。
③、
动态对象年龄判定
除了固定的年龄阈值,还会根据各个年龄段对象的存活大小和内存空间等因素动态调整对象的晋升策略。
比如说,在 Survivor 空间中相同年龄的所有对象大小总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。
error与execption异同
三分恶面渣逆袭:Java异常体系
Throwable
是 Java 语言中所有错误和异常的基类。它有两个主要的子类:Error 和 Exception,这两个类分别代表了 Java 异常处理体系中的两个分支。
Error 类代表那些严重的错误,这类错误通常是程序无法处理的。比如,OutOfMemoryError 表示内存不足,StackOverflowError 表示栈溢出。这些错误通常与 JVM 的运行状态有关,一旦发生,应用程序通常无法恢复。
Exception 类代表程序可以处理的异常。它分为两大类:编译时异常(Checked Exception)和运行时异常(Runtime Exception)。
try-catch-finally抛出异常,catch和finally的异常可以同时抛出吗
如果 catch 块抛出一个异常,而 finally 块中也抛出异常,那么最终抛出的将是 finally 块中的异常。catch 块中的异常会被丢弃,而 finally 块中的异常会覆盖并向上传递。
public class Example { public static void main (String[] args) { try { throw new Exception("Exception in try" ); } catch (Exception e) { throw new RuntimeException("Exception in catch" ); } finally { throw new IllegalArgumentException("Exception in finally" ); } } }
java多线程,同步与互斥
互斥,就是不同线程通过竞争进入临界区(共享数据或者硬件资源),为了防止冲突,在有限的时间内只允许其中一个线程独占使用共享资源。如不允许同时写。
同步,就是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含了互斥关系。同时,临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用。
在 Java 中,当我们要保护一个资源时,通常会使用 synchronized 关键字或者 Lock 接口的实现类(如 ReentrantLock)来给资源加锁。
锁在操作系统层面的意思就是 Mutex(互斥),意思就是某个线程获取锁(进入临界区)后,其他线程不能再进入临界区,这样就达到了互斥的目的。
cxuan:使用临界区的互斥
锁要处理的问题大概有四种:
谁拿到了锁,可以是当前 class,可以是某个 lock 对象,或者实例的 markword;
抢占锁的规则,只能一个人抢 Mutex;能抢有限多次(Semaphore);自己可以反复抢(可重入锁 ReentrantLock);读可以反复抢,写只能一个人抢(读写锁ReadWriteLock);
抢不到怎么办,等待,等待的时候怎么等,自旋,阻塞,或者超时;
锁被释放了还有其他等待锁的怎么办?通知所有人一起抢或者只告诉一个人抢(Condition 的 signalAll 或者 signal)
恰当地使用锁,就能解决同步或者互斥的问题。
synchronized ReentrantLock 区别
synchronized 是一个关键字,ReentrantLock是 Lock 接口的一个实现。
三分恶面渣逆袭:synchronized和ReentrantLock的区别
它们都可以用来实现同步,但也有一些区别:
ReentrantLock 可以实现多路选择通知(绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知);
ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放;synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放,不需要手动操作。
ReentrantLock 通常能提供更好的性能,因为它可以更细粒度控制锁;synchronized 只能同步代码快或者方法,随着 JDK 版本的升级,两者之间性能差距已经不大了。
互斥和同步在时间上有要求吗
互斥和同步在时间上是有一定要求的,因为它们都涉及到对资源的访问顺序和时机控制。
互斥的核心是保证同一时刻只有一个线程能访问共享资源或临界区。虽然互斥的重点不是线程执行的顺序,但它对访问的时间点有严格要求,以确保没有多个线程在同一时刻访问相同的资源。
同步强调的是线程之间的执行顺序和时间点的配合,特别是在多个线程需要依赖于彼此的执行结果时。例如,在 CountDownLatch 中,主线程会等待多个子线程的任务完成,子线程完成后才会减少计数,主线程会在计数器归零时继续执行。
class SyncExample { public static void main (String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3 ); // 创建3个子线程 for (int i = 0 ; i 3; i++) { new Thread(() -> { try { Thread.sleep(1000 ); // 模拟任务 System.out.println("打完王者了." ); } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); // 每个线程任务完成后计数器减1 } }).start(); } System.out.println("等打完三把王者就去睡觉..." ); latch.await(); // 主线程等待子线程完成 System.out.println("好,王者玩完了,可以睡了" ); } }
二哥的Java 进阶之路:CountDownLatch
操作系统内核对象实现同步与互斥?
同步解决的是多线程操作共享资源的问题,不管线程之间是如何穿插执行的,最后的结果都是正确的。
在操作系统层面,保证线程同步的方式有很多,比如锁、信号量等。那在此之前,需要先了解什么是临界区。
cxuan:使用临界区的互斥
临界区:对共享资源访问的程序片段,我们希望这段代码是
互斥
的,可以保证在某个时刻只能被一个线程执行,也就是说一个线程在临界区执行时,其它线程应该被阻止进入临界区。
临界区不仅针对线程,同样针对进程。同步的实现方式有:
①、
互斥锁
使⽤加锁操作和解锁操作可以解决并发线程/进程的互斥问题。
任何想进⼊临界区的线程,必须先执⾏加锁操作。若加锁操作顺利通过,则线程可进⼊临界区;在完成对临界资源的访问后再执⾏解锁操作,以释放该临界资源。
加锁和解锁锁住的是什么呢?可以是
临界区对象
,也可以只是一个简单的
互斥量
,例如互斥量是
0
无锁,
1
表示加锁。
根据锁的实现不同,可以分为
忙等待锁
和
⽆忙等待锁
。
忙等待锁(也称为自旋锁,Spinlock)是指当一个线程试图获取锁时,如果该锁已经被其他线程持有,当前线程不会立即进入休眠或阻塞,而是不断地检查锁的状态,直到该锁可用为止。这个过程被称为忙等待(busy waiting),因为线程在等待锁时仍然占用 CPU 资源,处于活跃状态。优点是避免了线程的上下文切换。
无忙等待锁是指当一个线程尝试获取锁时,如果锁已经被其他线程持有,当前线程不会忙等待,而是主动让出 CPU,进入阻塞状态或休眠状态,等待锁释放。当锁被释放时,线程被唤醒并重新尝试获取锁。这类锁的主要目的是避免忙等待带来的 CPU 资源浪费。
②、
信号量
信号量是操作系统提供的⼀种协调共享资源访问的⽅法。
通常表示资源的数量
,对应的变量是⼀个整型(sem)变量。
另外,还有
两个原⼦操作的系统调⽤函数来控制信号量
,分别是:
P
操作:当线程想要进入临界区时,会尝试执行 P 操作。如果信号量的值大于 0,信号量值减 1,线程可以进入临界区;否则,线程会被阻塞,直到信号量大于 0。
V
操作:当线程退出临界区时,执行 V 操作,信号量的值加 1,释放一个被阻塞的线程。
死锁的条件
产生死锁需要同时满足四个必要条件:
互斥条件
(Mutual Exclusion):资源不能被多个进程共享,即资源一次只能被一个进程使用。如果一个资源已经被分配给了一个进程,其他进程必须等待,直到该资源被释放。
持有并等待条件
(Hold and Wait):一个进程已经持有了至少一个资源,同时还在等待获取其他被占用的资源。在此期间,该进程不会释放已经持有的资源。
不可剥夺条件
(No Preemption):已分配给进程的资源不能被强制剥夺,只有持有该资源的进程可以主动释放资源。
循环等待条件
(Circular Wait):存在一个进程集合
,其中
等待
持有的资源,
等待
持有的资源,依此类推,直到
等待
持有的资源,形成一个进程等待环。
假设有两个进程
和
,以及两个资源
和
,一个简单的死锁场景是这样的:
在这种情况下,发生死锁的步骤如下: