JAVA问答第一篇
引言
这个专题则是带大家杀掉自己一个又一个盲区
希望大家战胜一个又一个问题,走向技术的峰顶
Q1:面向对象和面向过程的区别是什么?
-
面向过程是一种'执行者'的思维。它关注的是解决问题的具体步骤。在面向过程看来,数据是独立存在的,函数也是独立存在的。我们要手动把数据作为参数传递给函数,函数处理完再返回结果。这就像是我们自己下厨,买菜(获取数据)、洗菜(处理数据)、炒菜(执行逻辑)每一步都要自己亲力亲为,而且数据(食材)是暴露在外的,很容易被污染。
-
面向对象是一种'指挥者'的思维。它把数据(属性)和操作数据的函数(方法)封装在了一起,也就是我们说的'对象'。这时候,函数不再是独立的工具,而是对象自带的能力。我们不需要关心内部具体怎么实现,只需要向对象发送消息,让它自己去执行。这就像是我们去饭店点菜,我们不需要关心食材在哪,怎么洗,只需要告诉服务员'我要宫保鸡丁',厨师(对象)自己会搞定一切。
Q2:啥是IOC?以及创建的依赖注入方法?
IOC(控制反转)是一种将对象创建和依赖管理权从代码中剥离,转交给外部容器(如 Spring)的设计思想,它将传统的主动 new 对象转变为被动接收,实现了程序的解耦。
而依赖注入(DI)则是实现 IOC 的具体手段,主要包括三种方式:
- 构造函数注入(通过构造器传参,保证依赖不可变,是目前推荐的方式)
- Setter 方法注入(通过 setter 赋值,灵活性高但线程安全性需注意)
- 字段注入(通过反射直接赋值,虽然代码简洁但不利于测试,通常不推荐)。
通过 DI,容器负责在运行时动态建立对象间的关联,从而降低了组件间的耦合度,提升了代码的可维护性和可测试性。
Q3:synchronized和reentrantLock的区别简单说说?
这两个都是 Java 里用来保证线程安全的"锁",但它们的风格和能力差别挺大的。简单来说,synchronized 是 JVM 自带的"自动挡",简单安全;而 ReentrantLock 是 JDK 提供的"手动挡",功能强大且灵活。
核心区别速览
| 维度 | synchronized (自动挡) | ReentrantLock (手动挡) |
|---|---|---|
| 实现层面 | JVM 内置关键字,通过监视器(Monitor)实现 | JDK API 类,基于 AQS(队列同步器)实现 |
| 使用方式 | 自动获取/释放(进入/退出代码块自动完成) | 手动调用 lock() / unlock()(需配合 try/finally) |
| 公平锁 | 不支持(默认非公平) | 支持(构造函数可指定) |
| 可中断 | 不支持(等待锁时无法响应中断) | 支持 (lockInterruptibly()) |
| 超时获取 | 不支持(只能一直等) | 支持 (tryLock(timeout),可以放弃等待) |
| 条件队列 | 单一 (wait/notify,只能随机唤醒) |
多个 (Condition,可精准唤醒特定线程) |
详细解读
- 用法上的区别(自动 vs 手动)
- synchronized :它是 Java 的关键字,属于 JVM 层面的锁。你只需要在方法或代码块上加个关键字,JVM 会在编译时自动帮你加上"进入监视器"和"退出监视器"的指令。即使代码抛出异常,它也能自动释放锁,绝对不会忘,非常安全。
- ReentrantLock :它是一个具体的类。你需要显式地调用
lock()方法去获取锁,用完后必须在finally块中显式调用unlock()释放锁。如果你忘了 unlock,就会造成死锁,所以使用时要格外小心。
- 功能上的区别(基础 vs 高级)
这是两者最大的分水岭。
- synchronized 功能比较基础。它就像一把普通的锁,线程来了如果拿不到锁就只能排队死等,而且只有一个等待队列。
- ReentrantLock 功能非常丰富。
- 可尝试/超时获取 :你可以用
tryLock()尝试获取锁,如果拿不到可以先去干别的事;或者设定一个等待时间,超时就放弃。 - 可中断:线程在排队等锁时,如果不想等了,可以响应中断,不用死磕。
- 公平锁:你可以要求它严格按照"先来后到"的顺序获取锁(虽然默认也是非公平的,因为非公平性能更好)。
- 多条件唤醒 :它可以创建多个
Condition(条件队列),比如"队列不为空"和"队列不满",你可以精准地唤醒等待"不为空"的那一组线程,而不用像notify那样随机唤醒或全唤醒。
- 可尝试/超时获取 :你可以用
- 性能上的区别
- 在早期的 JDK 版本中,
synchronized性能较差(重量级锁),但自从 JDK 1.6 之后,它引入了偏向锁、轻量级锁等优化机制,性能已经大幅提升。 - 现在在大多数普通场景下,两者的性能差距已经很小了。
synchronized甚至在低竞争场景下表现更好 ,因为它是 JVM 内部优化的,而ReentrantLock毕竟是基于 API 层面的循环和 CAS 操作。
什么时候用哪个?
- 首选
synchronized:如果你只是想给一小段代码加个锁,保证线程安全,没有任何特殊需求,那就用synchronized。它写起来最简单,不容易出错,维护成本低。 - 选
ReentrantLock:当你需要尝试获取锁 (不想无限等待)、需要公平锁 (必须先来后到)、或者需要精准唤醒 特定线程时,才去用ReentrantLock。
一句话总结:能用 synchronized 解决的问题,尽量不要引入 ReentrantLock 增加复杂度;但如果业务场景需要高级功能,ReentrantLock 是唯一的选择。
Q4:CAS是啥
CAS 可以理解为一种**"乐观锁"**策略。传统的锁(如 synchronized)是悲观的,它假设"总会有人跟我抢",所以一上来就先把资源锁住(阻塞其他线程)。
而 CAS 是乐观的,它假设"没人跟我抢",所以它不会阻塞线程,而是直接去尝试修改数据。如果发现数据被别人改过了,它就放弃这次修改,或者重新读取数据再试一次。
工作原理(三个参数)
- V(Value) :内存地址中的当前值(你要修改的那个变量)。
- A(Expected Value) :预期值(你认为这个变量现在应该是多少)。
- B(New Value) :新值(你想把它改成多少)。
执行逻辑是这样的:
如果 内存地址 V 中的值等于我的预期值 A,那么 就把 V 的值更新为新值 B;否则,什么也不做(通常会循环重试)。
CAS 的典型应用
- 原子类: 比如
AtomicInteger、AtomicLong。当你调用incrementAndGet()(自增)时,底层就是通过循环 CAS 来实现的,而不是用的synchronized。 - 并发容器: 比如
ConcurrentHashMap(JDK 8+)、ConcurrentLinkedQueue,它们在进行节点更新或链表操作时,大量使用了 CAS 来保证线程安全,从而减少了锁的使用,提高了并发性能。 - AQS(AbstractQueuedSynchronizer): 像
ReentrantLock、CountDownLatch等锁的底层实现,核心也是靠 CAS 来修改状态(state)。
为什么不用全是 CAS?
虽然 CAS 很高效,但它也不是万能的,有三个经典的缺点:
-
① 循环开销(自旋):
如果并发冲突非常严重(大家都在抢),CAS 会一直失败,导致线程一直在"空转"(自旋),白白消耗 CPU 资源。
- 对比: 这时候
synchronized反而可能更好,因为它会让抢不到锁的线程进入阻塞状态,不消耗 CPU。
- 对比: 这时候
-
② ABA 问题:
这是一个经典的逻辑漏洞。假设值原来是 A,后来变成了 B,然后又变回了 A。CAS 在检查时发现"值还是 A",就会认为没人改过,从而操作成功。但实际上中间已经发生过变化了。
- 解决: Java 提供了
AtomicStampedReference类,通过引入版本号(每次修改版本号+1)来解决这个问题。
- 解决: Java 提供了
-
③ 只能保证单个变量的原子性:
CAS 只能对一个共享变量进行原子操作。如果你需要保证多个变量或者一段代码的原子性,CAS 就无能为力了,这时候还是得用锁。
Q5:泛型和基本类型的区别是啥?
- 概念维度的本质不同
- 基本类型 是 Java 语言内置的最基础的数据载体,比如
int、boolean、double。它们不是对象,直接存储在栈内存中,操作的是具体的"值",因此效率极高。 - 泛型 是一种编程语言的特性(参数化类型),比如
List<T>。它不是一种具体的数据,而是一种编写通用代码的模板。它允许我们在定义类、接口或方法时,把类型当作参数传递,从而实现类型安全和代码复用。
- 核心冲突:泛型不支持基本类型
这是两者结合时最需要注意的点。Java 的泛型不支持直接使用基本类型作为类型参数。
- 错误示例 :我们不能写
List<int>或Map<String, double>。 - 原因 :
- 类型擦除机制 :Java 泛型在编译后会被擦除,统一替换为
Object或其上限类型。而基本类型(如int)并不是Object的子类,无法进行统一的父类引用。 - JVM 兼容性:为了保持与旧版本字节码的兼容,JVM 指令集没有专门为泛型基本类型做修改。
- 类型擦除机制 :Java 泛型在编译后会被擦除,统一替换为
- 解决方案与性能权衡
既然不能直接用,Java 提供了包装类作为桥梁。
- 对应关系 :
int对应Integer,double对应Double等。 - 装箱与拆箱 :当我们把
int放入List<Integer>时,会发生装箱 (Autoboxing,变成对象);取出使用时会发生拆箱。 - 性能影响 :这意味着使用泛型存储数值时,实际上是存储的对象引用,且伴随着频繁的装箱拆箱操作,这会带来额外的堆内存开销和 CPU 消耗。因此,在高性能数值计算场景,原生数组(
int[])通常优于泛型集合。
总结:
基本类型追求的是极致的性能和内存效率 ;泛型追求的是代码的通用性和编译期的类型安全。两者通过包装类结合,虽然牺牲了一点性能,但换来了集合框架处理各种数据类型的统一能力。