JAVA进阶之路——无奖问答挑战1

JAVA问答第一篇

引言

这个专题则是带大家杀掉自己一个又一个盲区

希望大家战胜一个又一个问题,走向技术的峰顶

Q1:面向对象和面向过程的区别是什么?

  • 面向过程是一种'执行者'的思维。它关注的是解决问题的具体步骤。在面向过程看来,数据是独立存在的,函数也是独立存在的。我们要手动把数据作为参数传递给函数,函数处理完再返回结果。这就像是我们自己下厨,买菜(获取数据)、洗菜(处理数据)、炒菜(执行逻辑)每一步都要自己亲力亲为,而且数据(食材)是暴露在外的,很容易被污染。

  • 面向对象是一种'指挥者'的思维。它把数据(属性)和操作数据的函数(方法)封装在了一起,也就是我们说的'对象'。这时候,函数不再是独立的工具,而是对象自带的能力。我们不需要关心内部具体怎么实现,只需要向对象发送消息,让它自己去执行。这就像是我们去饭店点菜,我们不需要关心食材在哪,怎么洗,只需要告诉服务员'我要宫保鸡丁',厨师(对象)自己会搞定一切。


Q2:啥是IOC?以及创建的依赖注入方法?

IOC(控制反转)是一种将对象创建和依赖管理权从代码中剥离,转交给外部容器(如 Spring)的设计思想,它将传统的主动 new 对象转变为被动接收,实现了程序的解耦。

而依赖注入(DI)则是实现 IOC 的具体手段,主要包括三种方式:

  1. 构造函数注入(通过构造器传参,保证依赖不可变,是目前推荐的方式)
  2. Setter 方法注入(通过 setter 赋值,灵活性高但线程安全性需注意)
  3. 字段注入(通过反射直接赋值,虽然代码简洁但不利于测试,通常不推荐)。
    通过 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,可精准唤醒特定线程)

详细解读

  1. 用法上的区别(自动 vs 手动)
  • synchronized :它是 Java 的关键字,属于 JVM 层面的锁。你只需要在方法或代码块上加个关键字,JVM 会在编译时自动帮你加上"进入监视器"和"退出监视器"的指令。即使代码抛出异常,它也能自动释放锁,绝对不会忘,非常安全。
  • ReentrantLock :它是一个具体的类。你需要显式地调用 lock() 方法去获取锁,用完后必须在 finally 块中显式调用 unlock() 释放锁。如果你忘了 unlock,就会造成死锁,所以使用时要格外小心。
  1. 功能上的区别(基础 vs 高级)
    这是两者最大的分水岭。
  • synchronized 功能比较基础。它就像一把普通的锁,线程来了如果拿不到锁就只能排队死等,而且只有一个等待队列。
  • ReentrantLock 功能非常丰富。
    • 可尝试/超时获取 :你可以用 tryLock() 尝试获取锁,如果拿不到可以先去干别的事;或者设定一个等待时间,超时就放弃。
    • 可中断:线程在排队等锁时,如果不想等了,可以响应中断,不用死磕。
    • 公平锁:你可以要求它严格按照"先来后到"的顺序获取锁(虽然默认也是非公平的,因为非公平性能更好)。
    • 多条件唤醒 :它可以创建多个 Condition(条件队列),比如"队列不为空"和"队列不满",你可以精准地唤醒等待"不为空"的那一组线程,而不用像 notify 那样随机唤醒或全唤醒。
  1. 性能上的区别
  • 在早期的 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 的典型应用

  • 原子类: 比如 AtomicIntegerAtomicLong。当你调用 incrementAndGet()(自增)时,底层就是通过循环 CAS 来实现的,而不是用的 synchronized
  • 并发容器: 比如 ConcurrentHashMap(JDK 8+)、ConcurrentLinkedQueue,它们在进行节点更新或链表操作时,大量使用了 CAS 来保证线程安全,从而减少了锁的使用,提高了并发性能。
  • AQS(AbstractQueuedSynchronizer):ReentrantLockCountDownLatch 等锁的底层实现,核心也是靠 CAS 来修改状态(state)。

为什么不用全是 CAS?

虽然 CAS 很高效,但它也不是万能的,有三个经典的缺点:

  • ① 循环开销(自旋):

    如果并发冲突非常严重(大家都在抢),CAS 会一直失败,导致线程一直在"空转"(自旋),白白消耗 CPU 资源。

    • 对比: 这时候 synchronized 反而可能更好,因为它会让抢不到锁的线程进入阻塞状态,不消耗 CPU。
  • ② ABA 问题:

    这是一个经典的逻辑漏洞。假设值原来是 A,后来变成了 B,然后又变回了 A。CAS 在检查时发现"值还是 A",就会认为没人改过,从而操作成功。但实际上中间已经发生过变化了。

    • 解决: Java 提供了 AtomicStampedReference 类,通过引入版本号(每次修改版本号+1)来解决这个问题。
  • ③ 只能保证单个变量的原子性:

    CAS 只能对一个共享变量进行原子操作。如果你需要保证多个变量或者一段代码的原子性,CAS 就无能为力了,这时候还是得用锁。


Q5:泛型和基本类型的区别是啥?

  1. 概念维度的本质不同
  • 基本类型 是 Java 语言内置的最基础的数据载体,比如 intbooleandouble。它们不是对象,直接存储在栈内存中,操作的是具体的"值",因此效率极高。
  • 泛型 是一种编程语言的特性(参数化类型),比如 List<T>。它不是一种具体的数据,而是一种编写通用代码的模板。它允许我们在定义类、接口或方法时,把类型当作参数传递,从而实现类型安全和代码复用。
  1. 核心冲突:泛型不支持基本类型
    这是两者结合时最需要注意的点。Java 的泛型不支持直接使用基本类型作为类型参数。
  • 错误示例 :我们不能写 List<int>Map<String, double>
  • 原因
    • 类型擦除机制 :Java 泛型在编译后会被擦除,统一替换为 Object 或其上限类型。而基本类型(如 int)并不是 Object 的子类,无法进行统一的父类引用。
    • JVM 兼容性:为了保持与旧版本字节码的兼容,JVM 指令集没有专门为泛型基本类型做修改。
  1. 解决方案与性能权衡
    既然不能直接用,Java 提供了包装类作为桥梁。
  • 对应关系int 对应 Integerdouble 对应 Double 等。
  • 装箱与拆箱 :当我们把 int 放入 List<Integer> 时,会发生装箱 (Autoboxing,变成对象);取出使用时会发生拆箱
  • 性能影响 :这意味着使用泛型存储数值时,实际上是存储的对象引用,且伴随着频繁的装箱拆箱操作,这会带来额外的堆内存开销和 CPU 消耗。因此,在高性能数值计算场景,原生数组(int[])通常优于泛型集合。

总结:

基本类型追求的是极致的性能和内存效率 ;泛型追求的是代码的通用性和编译期的类型安全。两者通过包装类结合,虽然牺牲了一点性能,但换来了集合框架处理各种数据类型的统一能力。

相关推荐
华仔啊2 小时前
挖到了 1 个 Java 小特性:var,用完就回不去了
java·后端
SimonKing2 小时前
SpringBoot整合秘笈:让Mybatis用上Calcite,实现统一SQL查询
java·后端·程序员
日月云棠18 小时前
各版本JDK对比:JDK 25 特性详解
java
用户83071968408219 小时前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide19 小时前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家19 小时前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺19 小时前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java
用户9083246027320 小时前
Spring Boot + MyBatis-Plus 多租户实战:从数据隔离到权限控制的完整方案
java·后端
桦说编程20 小时前
实战分析 ConcurrentHashMap.computeIfAbsent 的锁冲突问题
java·后端·性能优化
程序员清风1 天前
用了三年AI,我总结出高效使用AI的3个习惯!
java·后端·面试