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关键字修饰且私有的,所以其不可被继承和改写,因此其是不可变的,创建一个新的字符串,其会开辟出一块新空间来存储。
StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
单线程用StringBuilder效率高、多线程用StringBuffer更安全。
5、“==”和equals方法的区别
==符号通常用于比较基本数据类型,equals方法通常用于比较引用数据类型,equals方法没有经过重写其实比较的还是引用指向的地址值和==作用类似,但是经过重写之后,比较的就是引用类型指向的值,比如String类型的equals方法比较的就是内部属性值。
6、throw和throws的区别:
throw
用于手动抛出异常对象,而throws
用于声明方法可能会抛出的异常类型。throw
是在方法内部使用的,用于抛出具体的异常对象;而throws
是在方法声明中使用的,用于指定方法可能会抛出的异常类型,以便调用者知道需要处理哪些异常。
7、反射的应用场景有哪些?
Java中的反射机制允许程序在运行时修改对象和类的行为。以下是反射机制的主要功能:
- 动态加载类:反射机制允许程序在运行时根据类的全限定名(包括包名和类名)来加载类,而不需要提前知道类的具体信息,面向AOP编程思想就是这么实现的。
- 获取类信息:通过反射可以获取到一个类的所有属性和方法,包括私有的、受保护的、公共的成员。这包括字段(成员变量)、方法、构造函数等信息。
- 操作属性和方法:反射不仅可以查看类的信息,还可以在运行时动态地调用方法或者访问、修改字段的值,即使这些方法是私有的也可以被调用。
- 创建对象实例:利用反射,可以在不知道具体类的情况下,通过类的全限定名来创建该类的实例对象。
- 实现通用代码:反射机制使得编写通用代码成为可能,例如框架中的对象映射、依赖注入等,是面向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
接口的集合比如TreeSet
或HashSet
,不需要就选择实现List
接口的比如ArrayList
或LinkedList
,然后再根据实现这些接口的集合的特点来选用。
4、TreeMap根据什么来排序
TreeMap的底层数据结构是红黑树,其是按照键的自然顺序来进行排序的。
5、说说HashMap和ConcurrentHashMap的区别(ConcurrentHashMap还与HashTable比较过):
首先就是HashMap是线程不安全的,ConcurrentHashMap是线程安全的,这是因为ConcurrentHashMap实现了分段锁的机制,粒度更细提高了并发性;同时ConcurrentHashMap在外部没有竞争的时候,优先使用乐观锁的算法CAS算法,提高了效率;HashMap允许使用null作为键或值,而ConcurrentHashMap不允许null作为键或值(为啥不允许null作为键值?并发情况下为了避免二义性)。
Java并发编程部分(被拷打的最多的地方):
1、创建线程有几种方式?
- 继承Thread类:通过创建一个继承自Thread类的子类,并重写其run方法来定义线程执行的任务。然后创建该子类的实例,并调用start方法来启动线程。这种方式简单直接,但由于Java不支持多重继承,它限制了类的扩展性。
- 实现Runnable接口:通过实现Runnable接口,并实现其run方法来定义线程任务。然后将实现了Runnable接口的类实例传递给Thread类的构造函数,再调用Thread实例的start方法来启动线程。这种方式避免了单一继承的局限性,一个类可以实现多个接口。
- 实现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 中的线程池提供了几种常见的饱和策略,可以根据具体情况选择适合的策略。以下是常见的几种线程池饱和策略:
-
AbortPolicy(默认策略):
- 当线程池无法处理新提交的任务时,会抛出
RejectedExecutionException
异常。 - 这是默认的饱和策略,适用于不希望丢失任何任务的情况,但需要对任务提交进行适当的控制,以避免任务丢失。
- 当线程池无法处理新提交的任务时,会抛出
-
CallerRunsPolicy:
- 当线程池无法处理新提交的任务时,由提交任务的线程(caller)来执行该任务。
- 这个策略不会抛出异常,而是将任务交给当前线程执行,从而保证任务不会丢失,但可能会影响当前线程的性能。
-
DiscardPolicy:
- 当线程池无法处理新提交的任务时,直接丢弃该任务,不做任何处理。
- 这种策略会导致部分任务丢失,适用于对任务丢失没有太大影响的情况。
-
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、为什么不提倡使用第二种线程池创造方法?
-
无法灵活配置线程池参数:
Executors
工厂类提供的方法通常只能创建标准配置的线程池,无法灵活配置线程池的参数,如核心线程数、最大线程数、任务队列类型等。 -
缺乏线程池拒绝策略:
Executors
工厂类创建的线程池默认使用ThreadPoolExecutor
的默认拒绝策略,即直接抛出RejectedExecutionException
异常。在实际应用中,往往需要根据业务需求自定义拒绝策略。
Java IO流:
1、大致介绍一下I/O流:
字节流用于以字节的形式进行输入和输出操作,适用于处理二进制数据。在 Java 中,字节流主要由 InputStream
和 OutputStream
类及其子类组成。
-
InputStream:抽象类,用于读取字节数据的基类。
- 子类:
FileInputStream
、ByteArrayInputStream
、BufferedInputStream
等。
- 子类:
-
OutputStream:抽象类,用于写入字节数据的基类。
- 子类:
FileOutputStream
、ByteArrayOutputStream
、BufferedOutputStream
等。
- 子类:
字符流用于以字符的形式进行输入和输出操作,适用于处理文本数据。在 Java 中,字符流主要由 Reader
和 Writer
类及其子类组成。
-
Reader:抽象类,用于读取字符数据的基类。
- 子类:
FileReader
、StringReader
、BufferedReader
等。
- 子类:
-
Writer:抽象类,用于写入字符数据的基类。
- 子类:
FileWriter
、StringWriter
、BufferedWriter
等。
- 子类:
2、 使用IO流时需要注意什么问题?
①、字节流是更适合处理视频、图片等这类数据的,不适合处理文本数据;字符流适合处理文本数据,不适合处理视频、图片一类的数据。二者混用会造成乱码问题。
②、对于一些大文件资源,可以使用缓冲池来进行缓存,减少io次数避免资源的浪费。
③、使用完毕后要及时用close()关闭流。
3、你的项目中用到了哪种IO模型?另外两种是什么?各有什么优势?
在 Java 中,常见的 I/O 模型包括阻塞 I/O 模型、非阻塞 I/O 模型和异步 I/O 模型。
-
阻塞 I/O 模型(Blocking I/O):
- 在阻塞 I/O 模型中,当应用程序发起 I/O 操作时,线程会被阻塞,直到操作完成或超时。
- 在 Java 中,传统的 I/O 类(如
InputStream
、OutputStream
、Reader
、Writer
)都是阻塞式的,例如InputStream
的read()
方法会在数据可用之前一直阻塞当前线程。
-
非阻塞 I/O 模型(Non-blocking I/O):
- 在非阻塞 I/O 模型中,应用程序可以立即返回而不必等待 I/O 操作完成,从而允许线程继续执行其他任务。
- 在 Java 中,通过
java.nio
包提供了非阻塞 I/O 支持,主要是通过SelectableChannel
和Selector
实现。在非阻塞模式下,调用Channel
的 I/O 操作方法(如read()
、write()
)不会阻塞线程,而是立即返回。
-
异步 I/O 模型(Asynchronous I/O):
- 在异步 I/O 模型中,应用程序发起 I/O 操作后可以继续执行其他任务,当操作完成时会通过回调函数或事件通知来处理结果。
- 在 Java 中,异步 I/O 主要通过
CompletableFuture
和CompletionHandler
来实现。在异步模式下,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包括哪些对象?(高频)
-
虚拟机栈中的引用:
虚拟机栈中的局部变量引用的对象,例如方法中的参数、局部变量、临时变量等。 -
本地方法栈中的引用:
本地方法栈中的JNI(Java Native Interface)方法引用的对象。 -
方法区中的类静态属性引用:
方法区中的类静态属性引用的对象,包括类的静态变量。 -
方法区中的常量引用:
方法区中的常量引用的对象,包括字符串常量、类的静态常量等。 -
Java堆中的类静态属性引用:
Java堆中的类静态属性引用的对象,与方法区中的类静态属性类似,但是这些引用是在运行时动态生成的。
9、JVM中的类加载机制
JVM中的类加载机制大致可以分为3步,分别是加载->连接->初始化。
加载是指将.class文件加载到内存中。连接可以大致分为三步,分别是验证、准备、解析,其中验证是指确保.class文件中的信息符合规范,准备是给该类在堆内存上划分空间并设置类变量初值,解析是将常量池中的符号引用替换为直接引用,初始化是指执行初始化方法clinit()。
10、JVM中对象加载机制
首先进行类加载检查,之后在堆内存中划分出一块区域,之后进行初始化零值,设置对象头,最后执行init()方法
11、init()方法和clinit()方法的区别
init()是方法构造器,clinit()是类构造器。init()方法在new一个对象时使用,clinit()方法在创建类时调用。
12、.class文件包含了什么?
.class文件包含了魔数、版本号,用来检验.Class文件信息;还包含了常量池,包含了符号引用和字面量;还有访问标志表示申明类型;索引表示继承关系;字段表包括类对象;方法表;属性表。