作为现在各类大中小企业面试程序员时的必问内容,“八股文”似乎是很重要的存在。但“八股文”是否能在实际工作中发挥它“敲门砖”应有的作用呢?有IT人士不禁发出疑问:程序员面试考什么?是工作能力、工作经验还是背题能力?还有程序员吐槽“八股文害人不浅呐,新来的应届生张口就是分布式和一堆框架,让他写代码根本就不会!”与此同时,也有支持“八股文”的声音:“熟练掌握“八股”,关键时刻出bug是真的能救命的。
“八股文”在程序员面试中的重要性和实际工作中的作用是一个备受争议的话题。我认为,这个问题的答案并不是简单的“助力”或者“阻力”,而是需要从多个角度来看待。
八股文是什么?
首先,需要明确“八股文”到底指的是什么。通常来说,程序员的“八股文”指的是一些经典的计算机基础知识和常见的技术问题,比如数据结构、算法、计算机网络、操作系统、数据库、常见的设计模式和典型的框架使用等。这些知识点往往是面试中的常见考点。
八股文的优点
-
基础知识的重要性:
- 掌握基础: 八股文涵盖了计算机科学的基础知识,这些基础知识是解决复杂问题的基石。了解数据结构和算法,理解计算机网络和操作系统的原理,可以帮助程序员在面对实际问题时,快速找到解决方案。
- 通用性强: 基础知识具有高度的通用性,不同的项目可能使用不同的框架和工具,但底层的计算机科学原理是不变的。
-
面试的公平性:
- 标准化: 八股文提供了一种相对标准化的面试流程,使得面试官在不同候选人之间进行比较时,有一个统一的标准。
- 筛选效率: 通过考察基础知识,面试官可以快速筛选出那些在计算机科学基础上比较扎实的候选人。
-
职业发展:
- 长远发展: 掌握扎实的基础知识,有助于程序员的长远职业发展。当新的技术出现时,能够迅速理解和掌握,因为新技术往往也是基于这些基础知识发展出来的。
八股文的缺点
-
与实际工作脱节:
- 实际工作中的问题: 实际工作中遇到的问题往往是复杂且多样化的,可能需要综合运用多种知识和经验。单纯依赖“八股文”可能无法解决实际问题。
- 动手能力: 有些人可能在背诵和理解理论知识上很强,但在实际编码和解决实际问题时表现不佳。
-
不关注软技能:
- 沟通和协作: 实际工作中,沟通和协作同样重要。面试中过度关注基础知识,可能忽略了候选人在团队协作、沟通能力、项目管理等方面的软技能。
- 实际项目经验: 基础知识固然重要,但实际的项目经验和解决问题的能力也是不可或缺的。面试中如果过分依赖“八股文”,可能无法全面评估候选人的实际工作能力。
综合观点
-
基础知识为先,但并非全部: 掌握扎实的基础知识是必要的,但这只是一个基础。面试中应结合候选人的实际项目经验、解决问题的能力、沟通和团队协作能力进行全面评估。
-
实际动手能力的重要性: 面试中可以通过实际编码测试、项目案例分析等方式,考察候选人的实际动手能力和解决问题的能力。这不仅能验证其基础知识的掌握程度,还能评估其实际工作能力。
-
平衡理论和实践: 面试官在设计面试题目时,应综合考虑基础知识和实际工作中的常见问题。通过多样化的考察方式,全面评估候选人的能力。
-
持续学习和成长: 无论是八股文还是实际工作经验,都是程序员职业发展的重要组成部分。程序员应保持持续学习的态度,不断提升自己的技能和知识储备。
结论
“八股文”在程序员面试中有其重要性,但不应成为唯一的评估标准。在实际工作中,扎实的基础知识和良好的动手能力、解决问题能力、团队协作能力同样重要。综合考虑多个方面,才能更全面地评估候选人的实际工作能力。
八股文面试题:
JAVA 锁
乐观 锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观 锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。
自旋 锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程
也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁
的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来
说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会
导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合
使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量
线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,
其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值 ( 1.6 引入了适应性自旋锁 )
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择
自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而
会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应
性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥
有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当
前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)
个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋
时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU
的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放
弃线程优先级之间的差异。
自旋锁的开启
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。
Synchronized 作用范围
1. 作用于方法时,锁住的是对象的实例(this);
2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen
(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,
会锁所有调用该方法的线程;
3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,
当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
Synchronized 核心 组件
1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
5) Owner:当前已经获取到所资源的线程被称为 Owner;
6) !Owner:当前释放锁的线程。
Java 锁机制
Java 提供了多种锁机制来处理并发编程中的同步问题。以下是几种常见的锁机制及其原理和使用场景:
1. 乐观锁(Optimistic Locking)
原理:
- 乐观锁假设并发冲突的可能性较低,因此在读取数据时不加锁。
- 在更新数据时,乐观锁会检查数据是否被其他线程修改过。通常通过版本号(version)或时间戳(timestamp)来实现。
- 如果版本号一致,则进行更新;否则,重试读取-比较-写入的过程。
实现:
- Java 中的乐观锁通常通过 CAS(Compare-And-Swap)操作实现。CAS 是一种原子操作,比较当前值与预期值,如果相同则更新,否则失败。
适用场景:
- 适用于读多写少的场景,减少锁的开销。
2. 悲观锁(Pessimistic Locking)
原理:
- 悲观锁假设并发冲突的可能性较高,因此在读取和写入数据时都加锁。
- 其他线程在尝试访问被锁定的数据时会被阻塞,直到锁被释放。
实现:
- Java 中的悲观锁可以通过
synchronized
关键字或ReentrantLock
实现。ReentrantLock
是基于 AQS(AbstractQueuedSynchronizer)框架的锁,先尝试使用 CAS 乐观锁获取锁,获取不到则转换为悲观锁。
适用场景:
- 适用于写操作频繁的场景,确保数据的一致性。
3. 自旋锁(Spin Lock)
原理:
- 自旋锁在等待锁释放时不会阻塞线程,而是让线程在循环中不断尝试获取锁。
- 如果持有锁的线程能在短时间内释放锁,自旋锁可以避免线程阻塞和上下文切换的开销。
优缺点:
- 优点:减少线程阻塞和上下文切换的开销,适用于锁竞争不激烈且锁持有时间短的场景。
- 缺点:自旋会消耗 CPU 资源,如果锁竞争激烈或持有时间长,自旋锁的消耗可能大于线程阻塞的开销。
实现:
- JDK 1.6 引入了适应性自旋锁,自动调整自旋时间。
- 可以通过 JVM 参数
-XX:+UseSpinning
和-XX:PreBlockSpin
控制自旋锁的行为(JDK 1.7 后由 JVM 自动控制)。
4. Synchronized 同步锁
原理:
synchronized
关键字可以锁住任意非 NULL 对象,属于独占式悲观锁和可重入锁。- 作用于方法时,锁住的是对象实例(
this
)。- 作用于静态方法时,锁住的是类的 Class 对象,相当于全局锁。
- 作用于代码块时,锁住的是指定的对象。
核心组件:
- Wait Set:存放调用
wait
方法被阻塞的线程。- Contention List:竞争队列,所有请求锁的线程首先被放在这里。
- Entry List:有资格成为候选资源的线程从竞争队列移动到这里。
- OnDeck:当前正在竞争锁资源的线程。
- Owner:当前持有锁的线程。
- !Owner:当前释放锁的线程。
适用场景:
- 适用于需要确保线程安全的场景,
synchronized
简单易用,但可能会导致性能瓶颈。
Java锁机制 示例代码 详解
以下是一些示例代码,展示如何使用这些锁机制:
乐观锁示例:
import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockExample { private AtomicInteger version = new AtomicInteger(0); public void updateData() { int currentVersion; do { currentVersion = version.get(); // 读取数据并进行操作 } while (!version.compareAndSet(currentVersion, currentVersion + 1)); // 更新数据 } }
悲观锁示例:
public class PessimisticLockExample { private final Object lock = new Object(); public void updateData() { synchronized (lock) { // 读取和更新数据 } } }
自旋锁示例:
public class SpinLockExample {
private volatile boolean isLocked = false;
public void lock() {
while (!compareAndSwap(false, true)) {
// 自旋等待
}
}
public void unlock() {
isLocked = false;
}
private boolean compareAndSwap(boolean expected, boolean newValue) {
if (isLocked == expected) {
isLocked = newValue;
return true;
}
return false;
}
}
Synchronized 同步锁示例:
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 线程安全的方法
}
public void synchronizedBlock() {
synchronized (this) {
// 线程安全的代码块
}
}
public static synchronized void staticSynchronizedMethod() {
// 静态方法的线程安全
}
}
这些示例展示了不同锁机制的基本用法,根据具体的应用场景选择合适的锁机制可以有效提高并发程序的性能和可靠性。