面试官:请简要介绍一下Java核心知识中面向对象的三大特性。
王铁牛:嗯,面向对象的三大特性就是封装、继承和多态嘛。封装就是把数据和操作数据的方法封装在一起,对外提供统一的接口;继承就是子类继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型而表现出不同的行为。
面试官:回答得不错。那在多线程环境下,如何保证数据的一致性?
王铁牛:这个嘛,我觉得可以用synchronized关键字来同步代码块或者方法,这样就能保证同一时间只有一个线程能访问共享资源,从而保证数据一致性。
面试官:很好。再问你一个,JUC包下有哪些常用的并发工具类?
王铁牛:嗯……有CountDownLatch、CyclicBarrier、Semaphore这些吧。
面试官:看来你对基础知识掌握得还可以。接下来进入第二轮,说说JVM的内存结构。
王铁牛:JVM内存结构包括堆、栈、方法区这些。堆是存放对象实例的地方,栈是存放局部变量和方法调用的地方,方法区存放类信息、常量、静态变量等。
面试官:那垃圾回收主要针对堆内存,常见的垃圾回收算法有哪些?
王铁牛:有标记清除算法、标记整理算法、复制算法、分代收集算法。
面试官:还不错。再问一个,如何查看JVM的内存使用情况?
王铁牛:可以用jconsole或者jvisualvm这些工具来查看。
面试官:好,现在进入第三轮。讲讲多线程中线程池的工作原理。
王铁牛:线程池就是预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。嗯……大概就是这样。
面试官:那如何合理配置线程池的参数?
王铁牛:这个……我不太清楚。
面试官:还有,线程池中的拒绝策略有哪些?
王铁牛:好像有AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
面试官:好的,今天的面试就到这里,回去等通知吧。
答案:
- 面向对象的三大特性:
- 封装:把对象的属性和操作方法包装在一起,对外只提供有限的接口。这样可以隐藏对象的内部实现细节,提高代码的安全性和可维护性。例如,在一个类中,将一些敏感数据的访问方法进行封装,只提供经过验证的公共方法来访问这些数据。
- 继承:子类继承父类的属性和方法。通过继承可以实现代码复用,减少重复代码。比如一个父类有一些通用的属性和方法,子类可以继承这些,然后根据自身需求进行扩展。
- 多态:同一个方法可以根据对象的不同类型而表现出不同的行为。分为编译时多态(方法重载)和运行时多态(方法重写)。方法重载是在同一个类中定义多个同名但参数不同的方法,调用时根据参数类型决定调用哪个方法;方法重写是子类重写父类的方法,运行时根据对象的实际类型决定调用哪个类的重写方法。
- 多线程环境下保证数据一致性:
- synchronized关键字:
- 同步代码块:通过在代码块前加上synchronized关键字,指定一个对象作为锁。当一个线程访问被synchronized修饰的代码块时,它首先会获取锁,如果锁被其他线程占用,那么该线程会进入等待状态,直到锁被释放。例如:
Object lock = new Object(); synchronized(lock) { // 共享资源操作代码 } - 同步方法:在方法声明前加上synchronized关键字,该方法就成为同步方法。同步方法使用的锁是当前对象(this)。比如:
public synchronized void syncMethod() { // 方法体 }
- 同步代码块:通过在代码块前加上synchronized关键字,指定一个对象作为锁。当一个线程访问被synchronized修饰的代码块时,它首先会获取锁,如果锁被其他线程占用,那么该线程会进入等待状态,直到锁被释放。例如:
- synchronized关键字:
- JUC包下常用的并发工具类:
- CountDownLatch:用于协调多个线程之间的同步。它维护一个计数器,通过调用countDown方法来减少计数器的值,当计数器的值为0时,等待在await方法上的线程会被唤醒。例如,多个线程需要等待某个初始化操作完成后再继续执行,可以使用CountDownLatch。
- CyclicBarrier:也用于线程同步,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的是,CyclicBarrier可以重复使用。当所有线程到达屏障点时,会执行一个可选的Runnable任务,然后屏障会重置,线程可以继续执行。
- Semaphore:用于控制对共享资源的访问权限。它维护一个许可证数量,通过acquire方法获取许可证,release方法释放许可证。当许可证数量为0时,acquire方法会阻塞,直到有许可证可用。可以用于限制同时访问某个资源的线程数量。
- JVM的内存结构:
- 堆:是JVM中最大的一块内存区域,用于存放对象实例。堆被分为新生代、老年代和永久代(Java 8后为元空间)。新生代又分为Eden区和两个Survivor区。对象首先在Eden区创建,当Eden区满时,会触发Minor GC,将存活的对象复制到Survivor区。如果Survivor区也满了,会将对象晋升到老年代。
- 栈:每个线程都有自己独立的栈空间,用于存放局部变量、方法调用等。栈中的数据是线程私有的,生命周期与线程相同。方法调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量和操作数。
- 方法区:用于存储类信息、常量、静态变量等。在Java 8之前,方法区也被称为永久代,存在内存溢出风险。Java 8后,方法区被元空间取代,元空间使用本地内存,减少了内存溢出的风险。
- 常见的垃圾回收算法:
- 标记清除算法:首先标记出所有需要回收的对象,然后统一回收被标记的对象。这种算法会产生大量不连续的内存碎片,导致后续分配大对象时可能无法找到足够的连续内存而再次触发垃圾回收。
- 标记整理算法:在标记清除算法的基础上,将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。解决了标记清除算法产生内存碎片的问题。
- 复制算法:将内存空间分为两块,每次只使用其中一块。当这一块内存使用完后,将存活的对象复制到另一块内存中,然后清理掉原来的那块内存。这种算法适用于新生代,因为新生代中对象存活率低。
- 分代收集算法:根据对象的存活周期不同,将堆内存分为不同的区域,针对不同区域采用不同的垃圾回收算法。比如新生代采用复制算法,老年代采用标记清除或标记整理算法。
- 查看JVM的内存使用情况:
- jconsole:是JDK自带的一个图形化工具,可以连接到正在运行的Java程序,实时监控JVM的内存、线程、类加载等信息。通过命令行输入“jconsole”即可启动。
- jvisualvm:也是JDK自带的工具,功能比jconsole更强大。它可以实时监控应用程序的性能指标,进行线程dump、内存dump等操作。同样可以通过命令行输入“jvisualvm”启动。
- 线程池的工作原理:
- 线程池预先创建一定数量的线程,这些线程处于空闲状态,等待任务的到来。
- 当有任务提交时,线程池会从线程池中获取一个线程来执行任务。
- 如果线程池中没有空闲线程,且当前线程数小于最大线程数,则会创建一个新的线程来执行任务。
- 如果线程数达到最大线程数,且队列已满,则会根据拒绝策略来处理新提交的任务。
- 线程执行完任务后,会返回到线程池中继续等待新的任务。
- 合理配置线程池的参数:
- corePoolSize:核心线程数,当提交的任务数小于corePoolSize时,线程池会创建新线程来执行任务。
- maximumPoolSize:最大线程数,当提交的任务数大于corePoolSize且队列已满时,会创建新线程直到线程数达到maximumPoolSize。
- keepAliveTime:线程池中的线程在空闲时的存活时间,当线程空闲时间超过keepAliveTime时,会被销毁。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,用于存放提交的任务。常用的有ArrayBlockingQueue、LinkedBlockingQueue等。
- threadFactory:线程工厂,用于创建线程,可以自定义线程的名称、优先级等。
- handler:拒绝策略,当线程数达到最大线程数且队列已满时,如何处理新提交的任务。
- 线程池中的拒绝策略:
- AbortPolicy:默认的拒绝策略,当线程池无法处理新任务时,会抛出RejectedExecutionException异常。
- CallerRunsPolicy:由调用线程来执行新提交的任务,这样可以降低新任务被拒绝的概率,但会影响调用线程的性能。
- DiscardPolicy:直接丢弃新提交的任务,不做任何处理。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务。