面试官:请简要介绍一下Java核心知识中面向对象的三大特性。
王铁牛:嗯,这个我知道,是封装、继承和多态。
面试官:不错,回答正确。那请说一下多态在实际开发中有什么作用呢?
王铁牛:多态可以提高代码的可扩展性和可维护性,比如在一个项目中,有很多不同类型的动物类,它们都继承自动物父类,当需要添加新的动物时,只需要创建新的子类即可,而不需要修改大量的调用代码。
面试官:很好,理解得很到位。接下来问你关于JUC的问题,什么是AQS?
王铁牛:这个……我想想,嗯……好像是一个什么队列之类的。
面试官:回答得不太准确,AQS即AbstractQueuedSynchronizer,是用来构建锁和同步器的框架,它使用一个FIFO队列来管理等待锁的线程。
第二轮: 面试官:说说JVM的内存结构。
王铁牛:有堆、栈、方法区这些。
面试官:那堆内存又分为哪几个区域?
王铁牛:嗯……不太清楚了。
面试官:堆内存分为新生代、老年代和永久代(现在是元空间)。新生代又分为Eden区和两个Survivor区。
面试官:多线程中如何实现线程安全?
王铁牛:用synchronized关键字吧。
面试官:除了synchronized,还有其他方式吗?
王铁牛:想不起来了。
面试官:还有Lock接口及其实现类,比如ReentrantLock,它可以更灵活地控制锁的获取和释放。
第三轮: 面试官:线程池的核心参数有哪些?
王铁牛:有corePoolSize、maximumPoolSize这些。
面试官:那它们具体的含义是什么?
王铁牛:这个……我不是特别确定了。
面试官:corePoolSize是线程池的核心线程数,当提交的任务数小于corePoolSize时,会创建新线程来执行任务;maximumPoolSize是线程池允许的最大线程数,当任务数大于corePoolSize时,会将任务放入队列中,如果队列满了且线程数小于maximumPoolSize,则会创建新线程来执行任务。
面试官:今天的面试就到这里,回去等通知吧。
答案:
- Java面向对象三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口。这样可以隐藏内部实现细节,提高代码的安全性和可维护性。例如,一个类中的成员变量可以使用private修饰,通过公有的getter和setter方法来访问和修改。
- 继承:子类继承父类的属性和方法,实现代码复用。子类可以扩展父类的功能,也可以重写父类的方法。比如,定义一个动物父类,然后创建猫、狗等子类继承自动物父类,子类可以继承父类的基本属性(如颜色、体重等)和行为(如移动方法),同时可以根据自身特点重写移动方法等。
- 多态:同一个行为具有多个不同表现形式或形态。在Java中,多态通过方法重写和父类引用指向子类对象来实现。如前面提到的动物类的例子,不同子类对象调用同一个方法(如移动方法),会表现出不同的行为。多态提高了代码的可扩展性和可维护性。
- AQS(AbstractQueuedSynchronizer):
- 它是用来构建锁和同步器的框架。
- 使用一个FIFO队列来管理等待锁的线程。当一个线程尝试获取锁失败时,它会被放入这个队列中等待。AQS通过一个int类型的变量来表示同步状态,比如在ReentrantLock中,0表示锁未被持有,大于0表示锁被持有,且数值表示持有锁的线程重入的次数。它提供了acquire和release等方法来实现锁的获取和释放逻辑,子类可以通过继承AQS并重写相关方法来实现自己的同步器。
- JVM内存结构:
- 堆:是JVM中最大的一块内存区域,用于存放对象实例。堆又分为新生代、老年代和永久代(现在是元空间)。
- 新生代:分为Eden区和两个Survivor区。新创建的对象一般会存放在Eden区,当Eden区满了,会触发一次Minor GC(新生代垃圾回收),存活的对象会被移动到其中一个Survivor区。经过多次Minor GC后,仍然存活的对象会被移动到老年代。
- 老年代:存放经过多次垃圾回收后仍然存活的对象。老年代的垃圾回收频率相对较低,但每次回收的时间较长。
- 元空间:在JDK8及以后,永久代被元空间取代。元空间主要存放类的元数据信息,如类的结构、方法定义等。它的大小不再受限于固定的内存大小,而是受限于系统的可用内存。
- 堆:是JVM中最大的一块内存区域,用于存放对象实例。堆又分为新生代、老年代和永久代(现在是元空间)。
- 多线程实现线程安全的方式:
- synchronized关键字:可以修饰方法或代码块。当一个线程访问被synchronized修饰的方法或代码块时,会先获取对象的锁,如果锁被其他线程持有,则该线程会进入等待状态,直到锁被释放。例如,在一个类中有一个被synchronized修饰的方法,当多个线程同时调用这个方法时,同一时间只能有一个线程进入该方法执行,其他线程需要等待。
- Lock接口及其实现类(如ReentrantLock):ReentrantLock提供了比synchronized更灵活的锁控制。它可以实现公平锁(多个线程按照申请锁的顺序获取锁),而synchronized是非公平锁。可以通过lock()方法获取锁,unlock()方法释放锁,并且可以在获取锁失败时进行更灵活的处理,比如设置等待时间等。例如,当一个线程调用lock()方法获取锁时,如果锁不可用,它可以选择等待一段时间或者放弃获取锁,而不像synchronized那样一直等待。
- 线程池的核心参数:
- corePoolSize:线程池的核心线程数。当提交的任务数小于corePoolSize时,线程池会创建新线程来执行任务。
- maximumPoolSize:线程池允许的最大线程数。当任务数大于corePoolSize时,会将任务放入队列中,如果队列满了且线程数小于maximumPoolSize,则会创建新线程来执行任务。
- keepAliveTime:线程池中的线程在空闲时的存活时间。当线程空闲时间超过keepAliveTime时,线程会被销毁,除非线程数小于corePoolSize。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,用于存放提交的任务,当线程数达到corePoolSize时,新提交的任务会放入这个队列中。
- threadFactory:线程工厂,用于创建线程,可自定义线程的名称、优先级等属性。
- handler:拒绝策略,当线程池的线程数达到maximumPoolSize且任务队列已满时,会调用handler来处理新提交的任务。常见的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。