面经总结(一)(java)

Java部分:

Java基础部分:

1、包装类与基本数据类型了解吗?二者区别是什么?

        基本数据类型包括八种,字符类型有byte和char分别占用1字节和2字节,整形有short、int、long分别占有2、4、8字节,浮点型有float和double,分别占有4、8字节,还有一个boolean类型。

        包装类对应为Byte、Character、Short、Integer、Long、Float、Double、Boolean

        包装类和基本数据类型区别就在于,包装类是以对象的形式将对应数据封装;基本数据类型是放在栈中的,包装类是放在堆内存中的;包装类实现了更多的功能,比如equals方法、hashCode方法、valueOf方法等,比基本数据类型更灵活;包装类和基本数据类型对象可以通过java的自动装箱拆箱来转换;在定义泛型时,尖括号中只能放对应的包装类。

2、Integer a = 120;Integer b = 120;a==b给出的boolean值是多少?

        由于包装类中Byte、Short、Integer、Long(整形)均实现了数据缓存机制,范围[-128,127],Character范围[0,127],所以a、b并没有在堆内存中自己开辟一块空间供自己使用,boolean值得到的时true。

3、JDK、JVM、JRE、JIT四者的区别和关系?

        JDK包含JRE包含JVM包含JIT,JDK中不仅仅有JRE还包含了其他许多开发工具,JRE包含了JVM和类库,JVM时java虚拟机,负责解释.class文件,JIT的存在是为了优化JVM的解释过程,提高速度。

4、StringBuffer、StringBuilder、String的区别与联系?

        String是经过final关键字修饰且私有的,所以其不可被继承和改写,因此其是不可变的,创建一个新的字符串,其会开辟出一块新空间来存储。

   StringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

       单线程用StringBuilder效率高、多线程用StringBuffer更安全。

5、“==”和equals方法的区别

        ==符号通常用于比较基本数据类型,equals方法通常用于比较引用数据类型,equals方法没有经过重写其实比较的还是引用指向的地址值和==作用类似,但是经过重写之后,比较的就是引用类型指向的值,比如String类型的equals方法比较的就是内部属性值。

6、throw和throws的区别:

        throw用于手动抛出异常对象,而throws用于声明方法可能会抛出的异常类型。throw是在方法内部使用的,用于抛出具体的异常对象;而throws是在方法声明中使用的,用于指定方法可能会抛出的异常类型,以便调用者知道需要处理哪些异常。

7、反射的应用场景有哪些?

Java中的反射机制允许程序在运行时修改对象和类的行为。以下是反射机制的主要功能:

  1. 动态加载类:反射机制允许程序在运行时根据类的全限定名(包括包名和类名)来加载类,而不需要提前知道类的具体信息,面向AOP编程思想就是这么实现的。
  2. 获取类信息:通过反射可以获取到一个类的所有属性和方法,包括私有的、受保护的、公共的成员。这包括字段(成员变量)、方法、构造函数等信息。
  3. 操作属性和方法:反射不仅可以查看类的信息,还可以在运行时动态地调用方法或者访问、修改字段的值,即使这些方法是私有的也可以被调用。
  4. 创建对象实例:利用反射,可以在不知道具体类的情况下,通过类的全限定名来创建该类的实例对象。
  5. 实现通用代码:反射机制使得编写通用代码成为可能,例如框架中的对象映射、依赖注入等,是面向AOP编程思想的实现基础。

        总的来说,Java的反射机制是一种在运行时检查和修改对象和类行为的能力,它提供了极大的灵活性,但同时也带来了额外的复杂性和潜在的性能开销。在使用反射时,应当权衡其带来的便利与可能的影响。

   

Java集合部分:

1、说一说集合底层的数据结构包含什么?

        集合整体分类可以分为单列集合和双列集合,单列集合包含了List、Set,双列集合指Map。其中List中常见的有两种类型,ArrayList、LinkedList,Arraylist底层是动态数组,Linkedlist底层是双向链表;Set常用的有HashSet和TreeSet两种,其底层实现原理是通过HashMap和TreeMap实现的;HashMap底层数据结构是哈希表,即数组+链表,JDK1.8之后引入了红黑树,而TreeMap底层的数据结构是红黑树。

2、线程安全的集合有哪些?

        线程安全的集合包含第一代集合,HashTable、Vector。第三代集合有ConcurrentHashMap,也是线程安全的。

3、两两比较区别,应用场景?

        三大集合的区别:List是有序的,且可重复的有索引的;Set是不可重复的;Map的键是不可重复的,值是可重复的。

        应用场景:

我们主要根据集合的特点来选择合适的集合。比如:

  • 我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap
  • 我们只需要存放元素值时,就选择实现Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSetHashSet,不需要就选择实现 List 接口的比如 ArrayListLinkedList,然后再根据实现这些接口的集合的特点来选用。

4、TreeMap根据什么来排序

       TreeMap的底层数据结构是红黑树,其是按照键的自然顺序来进行排序的。

5、说说HashMap和ConcurrentHashMap的区别(ConcurrentHashMap还与HashTable比较过):

        首先就是HashMap是线程不安全的,ConcurrentHashMap是线程安全的,这是因为ConcurrentHashMap实现了分段锁的机制,粒度更细提高了并发性;同时ConcurrentHashMap在外部没有竞争的时候,优先使用乐观锁的算法CAS算法,提高了效率;HashMap允许使用null作为键或值,而ConcurrentHashMap不允许null作为键或值(为啥不允许null作为键值?并发情况下为了避免二义性)。

Java并发编程部分(被拷打的最多的地方):

1、创建线程有几种方式?

  1. 继承Thread类:通过创建一个继承自Thread类的子类,并重写其run方法来定义线程执行的任务。然后创建该子类的实例,并调用start方法来启动线程。这种方式简单直接,但由于Java不支持多重继承,它限制了类的扩展性。
  2. 实现Runnable接口:通过实现Runnable接口,并实现其run方法来定义线程任务。然后将实现了Runnable接口的类实例传递给Thread类的构造函数,再调用Thread实例的start方法来启动线程。这种方式避免了单一继承的局限性,一个类可以实现多个接口。
  3. 实现Callable接口:Callable接口与Runnable类似,但它有返回值,并且可以抛出异常。使用FutureTask将Callable包装为一个异步任务,然后将FutureTask传递给Thread类或ExecutorService来执行。这种方式适用于需要计算结果的场景。

2、与操作系统的交叉问题,比如线程与进程的关系,线程死锁问题以及解决方式等。

3、怎么确保java多线程开发下的线程安全性:

        ①加锁:

          重量级锁:

使用synchronized关键字:这是Java中最基本的锁机制。通过在方法或者代码块上使用synchronized关键字,可以保证多个线程访问共享资源时的互斥性,确保同一时刻只有一个线程能够访问共享资源。

使用Lock锁:Java提供了显式的锁机制,如ReentrantLock,这种锁比synchronized提供了更高的灵活性,例如可以尝试获取锁并在无法获取时回退,或者设置锁的公平性等。

轻量级锁:

使用volatile关键字:volatile可以保证变量的可见性,当一个线程修改了一个volatile变量的值,其他线程立即可以看到这个变化。但它并不能保证原子性,所以在需要原子操作的情况下,还需要结合其他同步机制。

实现乐观锁:实现乐观锁一般有两种方式,一种是使用版本号机制,一种是使用CAS算法。

        ②使用线程安全的集合:Java标准库提供了一些线程安全的集合类,如ConcurrentHashMap、Hashtable、vector等,这些集合内部已经实现了必要的同步措施,可以在多线程环境下安全使用。

        ③使用ThreadLocal变量实现线程隔离,尽量减少共享变量的使用,或者设计成不可变对象,这样可以避免多线程之间的数据竞争。

        ④使用wait()、notify()和notifyAll()等方法进行线程间的通信,以确保线程间的协调和合作。

4、说说使用ThreadLocal变量时有什么注意事项:

        ThreadLocal变量存储在每个线程的ThreadLocalMap中,他的引用是弱引用,也就是在下次gc时候会把这个引用回收,但是这个value值是一个强引用,这样就容易导致内存泄漏的问题,因此要及时手动回收没有用的ThreadLocal变量。

5、Synchronized实现原理?

        底层原理涉及 Java 虚拟机 (JVM) 中的监视器锁 (Monitor Lock) 机制。

        当一个线程进入一个 synchronized 代码块或方法时,它会尝试获取对象的监视器锁。如果这个锁没有被其他线程占用,那么该线程将获得锁,并且可以继续执行 synchronized 区域的代码。如果锁已经被其他线程占用,那么该线程将被阻塞,直到锁被释放。

6、Synchronized和Volatile的比较:

        ①、Synchronized相较于Volatile是一种重量级的锁,Volatile实现了轻量级的同步机制,性能更好。

        ②、Synchronized实现了并发编程三大特性中的所有(原子性、有序性、可见性),而Volatile只实现了有序性和可见性。

        ③、Volatile主要就是用来解决变量可见性问题的,其会强行将线程修改的变量信息同步到内存中,使得其他线程第一时间可以看到,因此Volatile主要作用于变量,Synchronized主要作用于代码块或方法。

7、说说可重入锁的特点:

        Synchronized和Reentrantlock都是可重入锁,其又名为递归锁,这种机制使得同一个线程可以多次获得同一个锁,而不会因为自己已经持有锁而被阻塞。这在一些情况下非常有用,比如在递归函数中需要多次获取同一把锁的情况下,可重入锁能够避免死锁的发生。

8、比较ReentrantLock和Synchronized

        ①、二者都是递归锁。

        ②、ReentrantLock是基于API实现的,Synchronized是基于JVM来实现的。

        ③、ReentrantLock相比较于Synchronized来看,更加灵活,因为其实现了公平锁(Synchronized只能实现非公平锁)

9、比较ReentrantLock和ReetrantReadWritelLock,为啥要用读写锁?你怎么保证读写一致? (我项目里有用读写锁,被拷打过这个问题)

        第一个问题:因为读写锁的粒度更细,ReentrantLock粒度粗,上这种锁会导致系统陷入可串行化,但是读写锁对读写操作进行了划分,读锁实际上是共享锁,写锁实际上是排他锁,直接上可重入锁的话整个系统就会变成可串行化,但是上读写锁,可以保证写操作不被打断,且写入期间不允许再上任何其他的锁,读操作允许多个线程去同时读,从而提高了系统的性能。

        第二个问题:在对共享数据进行读写操作时,确保在同一时间只有一个线程能够修改数据。这就是使用读写锁的目的。写操作会独占锁,而读操作可以并发进行,但在写操作期间不允许读操作,从而确保了一致性。

10、线程池的核心参数有哪些? 

corePoolSize:指定了线程池中的线程数量。

maximumPoolSize:指定了线程池中的最大线程数量。

keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。

workQueue:任务队列,被提交但尚未被执行的任务。

threadFactory:线程工厂,用于创建线程,一般用默认的即可。

handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

11、线程池的运行流程?

                                                                                                             (图片引自JavaGuide网站)

12、核心参数ThreadFactory知道吧?举个例子

        ThreadFactory 是 Java 中用于创建线程的工厂接口,它允许你自定义创建线程的过程。通常情况下,线程池会使用 ThreadFactory 来创建新的线程。

        用法eg:可以实现ThreadFactory接口,来自定义创建线程的名字、线程的优先级

import java.util.concurrent.ThreadFactory;

public class CrawlerThreadFactory implements ThreadFactory {
    private int threadCount;

    public CrawlerThreadFactory() {
        this.threadCount = 0;
    }

    @Override
    public Thread newThread(Runnable r) {
        // 创建新线程
        Thread thread = new Thread(r);
        // 设置线程名称
        thread.setName("CrawlerThread-" + (++threadCount));
        // 设置线程优先级
        thread.setPriority(Thread.MIN_PRIORITY);
        return thread;
    }
}

// 在使用线程池时,使用自定义的线程工厂来创建线程
ExecutorService executor = Executors.newFixedThreadPool(5, new CrawlerThreadFactory());

13、饱和策略:

        线程池的饱和策略是指当线程池无法处理新提交的任务时,如何处理这些任务的策略。Java 中的线程池提供了几种常见的饱和策略,可以根据具体情况选择适合的策略。以下是常见的几种线程池饱和策略:

  1. AbortPolicy(默认策略)

    • 当线程池无法处理新提交的任务时,会抛出 RejectedExecutionException 异常。
    • 这是默认的饱和策略,适用于不希望丢失任何任务的情况,但需要对任务提交进行适当的控制,以避免任务丢失。
  2. CallerRunsPolicy

    • 当线程池无法处理新提交的任务时,由提交任务的线程(caller)来执行该任务。
    • 这个策略不会抛出异常,而是将任务交给当前线程执行,从而保证任务不会丢失,但可能会影响当前线程的性能。
  3. DiscardPolicy

    • 当线程池无法处理新提交的任务时,直接丢弃该任务,不做任何处理。
    • 这种策略会导致部分任务丢失,适用于对任务丢失没有太大影响的情况。
  4. DiscardOldestPolicy

    • 当线程池无法处理新提交的任务时,丢弃任务队列中等待时间最长的任务,然后尝试将新任务加入到任务队列中。
    • 这种策略可以确保有新的任务能够被执行,但可能会导致较早的任务被丢弃。

14、构造线程池的方式:

        1、通过ThreadPoolExcutor构造方法来构造:

ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

        2、通过Excutors构造函数来构造:

ExecutorService executor = Executors.newFixedThreadPool(nThreads);
ExecutorService executor = Executors.newCachedThreadPool();
ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newScheduledThreadPool(corePoolSize);

15、为什么不提倡使用第二种线程池创造方法?

  1. 无法灵活配置线程池参数Executors 工厂类提供的方法通常只能创建标准配置的线程池,无法灵活配置线程池的参数,如核心线程数、最大线程数、任务队列类型等。

  2. 缺乏线程池拒绝策略Executors 工厂类创建的线程池默认使用 ThreadPoolExecutor 的默认拒绝策略,即直接抛出 RejectedExecutionException 异常。在实际应用中,往往需要根据业务需求自定义拒绝策略。

 Java IO流:

 1、大致介绍一下I/O流:

        字节流用于以字节的形式进行输入和输出操作,适用于处理二进制数据。在 Java 中,字节流主要由 InputStreamOutputStream 类及其子类组成。

  • InputStream:抽象类,用于读取字节数据的基类。

    • 子类FileInputStreamByteArrayInputStreamBufferedInputStream 等。
  • OutputStream:抽象类,用于写入字节数据的基类。

    • 子类FileOutputStreamByteArrayOutputStreamBufferedOutputStream 等。

        字符流用于以字符的形式进行输入和输出操作,适用于处理文本数据。在 Java 中,字符流主要由 ReaderWriter 类及其子类组成。

  • Reader:抽象类,用于读取字符数据的基类。

    • 子类FileReaderStringReaderBufferedReader 等。
  • Writer:抽象类,用于写入字符数据的基类。

    • 子类FileWriterStringWriterBufferedWriter 等。

2、 使用IO流时需要注意什么问题?

        ①、字节流是更适合处理视频、图片等这类数据的,不适合处理文本数据;字符流适合处理文本数据,不适合处理视频、图片一类的数据。二者混用会造成乱码问题。

        ②、对于一些大文件资源,可以使用缓冲池来进行缓存,减少io次数避免资源的浪费。

        ③、使用完毕后要及时用close()关闭流。

3、你的项目中用到了哪种IO模型?另外两种是什么?各有什么优势?

        在 Java 中,常见的 I/O 模型包括阻塞 I/O 模型、非阻塞 I/O 模型和异步 I/O 模型。

  1. 阻塞 I/O 模型(Blocking I/O)

    • 在阻塞 I/O 模型中,当应用程序发起 I/O 操作时,线程会被阻塞,直到操作完成或超时。
    • 在 Java 中,传统的 I/O 类(如 InputStreamOutputStreamReaderWriter)都是阻塞式的,例如 InputStreamread() 方法会在数据可用之前一直阻塞当前线程。
  2. 非阻塞 I/O 模型(Non-blocking I/O)

    • 在非阻塞 I/O 模型中,应用程序可以立即返回而不必等待 I/O 操作完成,从而允许线程继续执行其他任务。
    • 在 Java 中,通过 java.nio 包提供了非阻塞 I/O 支持,主要是通过 SelectableChannelSelector 实现。在非阻塞模式下,调用 Channel 的 I/O 操作方法(如 read()write())不会阻塞线程,而是立即返回。
  3. 异步 I/O 模型(Asynchronous I/O)

    • 在异步 I/O 模型中,应用程序发起 I/O 操作后可以继续执行其他任务,当操作完成时会通过回调函数或事件通知来处理结果。
    • 在 Java 中,异步 I/O 主要通过 CompletableFutureCompletionHandler 来实现。在异步模式下,I/O 操作会立即返回一个 Future 或通过回调函数来处理操作完成的事件。

       每种 I/O 模型都有其适用的场景和优缺点。阻塞 I/O 模型简单易用,但可能导致资源浪费和性能下降;非阻塞 I/O 模型可以提高并发性能,但编程复杂度较高;异步 I/O 模型适合处理大量并发请求,但编程模型较为复杂。在实际应用中,需要根据具体需求和场景选择合适的 I/O 模型。

        

JVM虚拟机:

1、 java内存区域的划分:

        运行时分为堆内存、各个线程以及本地内存,线程中包括了PC程序计数器、虚拟机栈和本地方法栈,本地内存包含了直接内存以及常量池。

 2、pc程序计数器有什么作用?

        pc程序计数器有两个作用,一个是记录执行下一条指令的位置,另一个是在线程切换时,涉及到上下文的切换,pc负责记录断点的信息和加载新线程的位置。

3、JVM中栈如何划分?

        JVM中栈可以被划为两个部分,分别是虚拟机栈和本地方法栈。其中虚拟机栈负责实现java的方法,栈由一个个栈帧组成,每个栈帧中又包含了局部变量表、操作数栈、动态链接等信息。本地方法栈用于服务native方法,一般由c语言编写。

4、JVM中堆内存又是怎么划分的?

        可以划分为新生代、老生代和永生代(看JDK版本)。新生代又可以划分为Eden区、from survival区和to survival区三个部分。new了一个新对象,会优先分配在Eden区,之后通过垃圾回收算法转移到surrvival区再进行转移。

5、JVM中的垃圾回收算法有哪些?

        标记-清除、标记-复制、标记-整理算法。

        标记-清除:指标记出不使用的对象,然后进行清除。

        标记-复制:先copy出一块完全相同的区域,再将还存活的对象转移到该区域上,再对目标区域进行整体清除。

        标记-整理:将存活的对象移动到一端,再进行整体的清除操作。

6、JVM中的垃圾回收机制:       

        新生代内存用于存放新创建的对象,通常采用标记-复制算法进行垃圾回收。当Eden空间填满时,将触发一次新生代的垃圾回收(Minor GC),将不再存活的对象清理掉,并将存活的对象复制到Survivor空间中。如果Survivor空间也满了,则存活的对象会被复制到另一个Survivor空间中。这个过程会频繁地进行,用于清理短时间存活的对象。

        老生代通常采用标记-清除算法(Mark-Sweep)或标记-整理算法(Mark-Compact)进行垃圾回收。当老年代内存中的空间不足时,将触发一次老年代的垃圾回收(Major GC/Full GC),对老年代中的不再存活的对象进行清理。

        因为新生代不稳定,时而发生对象死去现象,采用标记复制效率高;而老生代则相对稳定,可以采用标记-清除和标记-整理算法。

7、死亡对象判断方法?

        两种方法,一种是引用计数器法,另一种是可达性分析算法。

        引用计数器法是一种简单的垃圾回收算法,其核心思想是给每个对象维护一个引用计数器,用于记录该对象被引用的次数。当一个对象被引用时,其引用计数器加一;当一个对象不再被引用时,其引用计数器减一。当引用计数器的值为零时,说明该对象不再被引用,可以被回收。

        优点:实现简单。

        缺点:无法解决对象循环引用问题。

        可达性分析算法是一种基于对象引用链的垃圾回收算法,其核心思想是通过一组称为"GC Roots"的根对象开始,进行一次遍历,标记所有被这些根对象直接或间接引用到的对象为存活对象,未被标记的对象则被认为是不可达的垃圾对象,可以被回收。

        优点:可以解决循环引用问题。

        缺点:每次扫描都是全局扫描,需要进行全局的对象引用扫描,对整个对象图进行遍历,当对象数量很大时,可能会造成较大的性能开销。

8、可达性分析算法中的GC Roots包括哪些对象?(高频)

  1. 虚拟机栈中的引用

    虚拟机栈中的局部变量引用的对象,例如方法中的参数、局部变量、临时变量等。
  2. 本地方法栈中的引用

    本地方法栈中的JNI(Java Native Interface)方法引用的对象。
  3. 方法区中的类静态属性引用

    方法区中的类静态属性引用的对象,包括类的静态变量。
  4. 方法区中的常量引用

    方法区中的常量引用的对象,包括字符串常量、类的静态常量等。
  5. Java堆中的类静态属性引用

    Java堆中的类静态属性引用的对象,与方法区中的类静态属性类似,但是这些引用是在运行时动态生成的。

9、JVM中的类加载机制

        JVM中的类加载机制大致可以分为3步,分别是加载->连接->初始化。

        加载是指将.class文件加载到内存中。连接可以大致分为三步,分别是验证、准备、解析,其中验证是指确保.class文件中的信息符合规范,准备是给该类在堆内存上划分空间并设置类变量初值,解析是将常量池中的符号引用替换为直接引用,初始化是指执行初始化方法clinit()。

10、JVM中对象加载机制

        首先进行类加载检查,之后在堆内存中划分出一块区域,之后进行初始化零值,设置对象头,最后执行init()方法

11、init()方法和clinit()方法的区别

        init()是方法构造器,clinit()是类构造器。init()方法在new一个对象时使用,clinit()方法在创建类时调用。

12、.class文件包含了什么?

        .class文件包含了魔数、版本号,用来检验.Class文件信息;还包含了常量池,包含了符号引用和字面量;还有访问标志表示申明类型;索引表示继承关系;字段表包括类对象;方法表;属性表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值