2022最新70道常见的Java面试题及答案,看看你会几道?
1、什么是垃圾回收及其优势和缺点?
垃圾收集是查看堆内存,识别正在使用的对象和未使用的对象以及删除未使用的对象的过程.
使用中的对象或引用的对象意味着程序的某些部分仍维护着指向该对象的指针.程序的任何部分都不再引用未使用的对象或未引用的对象.因此,可以回收未引用对象使用的内存.
优点是,它减轻了我们手动分配/释放内存的负担,因此我们可以专注于解决手头的问题.
缺点是.每当垃圾收集器运行时,它都会影响应用程序的性能.这是因为必须停止应用程序中的所有其他线程,以允许垃圾回收器线程有效地完成其工作.
根据应用程序的要求,这可能是客户无法接受的实际问题.但是,通过熟练的优化和垃圾收集器调整以及使用不同的GC算法,可以大大减少甚至消除此问题.
2、如何从Java代码触发垃圾回收?
仅当JVM认为它需要基于Java堆大小的垃圾回收时才会触发.
在从内存中删除对象之前,垃圾回收线程会调用该对象的finalize()方法,并提供执行所需的各种清理的机会.您也可以调用目标代码的此方法,但是,不能保证调用此方法时将发生垃圾回收.
此外,还有诸如System.gc()和Runtime.gc()之类的方法,这些方法用于将垃圾收集请求发送到JVM,但不能保证会发生垃圾收集.
当没有足够的堆空间来容纳新对象时会发生什么?
如果在Heap中没有用于创建新对象的内存空间,则Java虚拟机将抛出_OutOfMemoryError_或更确切地说是**java.lang.OutOfMemoryError**
3、什么是Stringbuilder及其用例?
StringBuilder允许通过追加,删除和插入字符和字符串来操纵字符序列.与不可变的String类相反,这是一种可变的数据结构.
4、将字符串附加到Stringbuilder和使用+运算符连接两个字符串之间有什么区别?
连接两个String实例时,将创建一个新对象,并复制字符串.如果我们需要在循环中创建或修改字符串,这可能会带来巨大的垃圾回收器开销StringBuilder允许更有效地处理字符串操作.
5、Stringbuilder与Stringbuffer有何不同?
StringBuffer和StringBuilder不同,它是线程安全的.如果需要在单个线程中处理字符串,请改用StringBuilder.
6、Java异常处理的最佳实践
尝试块中使用了许多资源,之后需要关闭它们.但是,不应在try块的末尾关闭这些资源,因为如果引发任何异常,则可能永远无法访问这些资源.因此,所有清理代码都应放在finally块中,以获得更好的结果.
所有异常和错误的超类都是可抛出的.绝对不要在catch子句中使用它,因为它将捕获所有异常和错误,其中某些异常和错误可能不在应用程序的控制范围内,因此无法处理.
描述性消息应提供例外,以帮助理解为什么将例外报告给监视工具或日志文件.描述性消息应准确,并尽可能清晰地描述异常事件问题.
方法签名中指定的所有异常也应记录在Javadoc中.这非常有用,因为它为调用者提供了更多信息,有助于按需处理或避免异常.
在永远不会发生的假设下,切勿忽略异常.这是有缺陷的,因为将来代码可能会以无法预料的方式更改,并且可能永远不会发生认为不需要的异常(因为特定事件永远不会发生).
应该首先捕获最具体的异常类,然后在执行与异常匹配的第一个捕获块时,提供较不具体的捕获块.因此,如果首先给出不太具体的异常捕获块,则控件可能永远不会到达更具体的异常捕获块.
最好有尽可能特殊的异常,因为它们会使API易于理解.这意味着应使用最适合异常事件的类,并应避免未指定的异常.
不要记录并重新抛出异常,因为它会导致同一异常的多个错误消息.这些额外的错误消息非常有用,因为它们不提供任何额外的信息.如果需要任何其他信息,则应捕获异常并将其包装在定制的异常中.
7、Java中ClassLoader的作用
编译后,Java类以字节代码的形式存储在.class文件中.需要时,ClassLoader将Java程序的类加载到内存中.
ClassLoader是分层的,因此,如果有加载类的请求,则会将其委派给父类加载器.使用此方法可以维护Java Runtime Environment中的唯一性.
Java中内置的ClassLoader的类型如下:
Bootstrap类加载器
扩展类加载器
系统类加载器
8、堆栈和堆内存有什么区别?为什么要使用堆栈来存储局部变量?
堆栈内存用于在通过线程执行方法时创建局部变量和对象引用.这意味着每个线程都有一个单独的堆栈和一组局部变量.堆栈不包含实际的对象-它仅包含引用.实际对象的内存空间在堆内存中分配.堆内存由许多部分组成-年轻一代(伊甸园和幸存者空间)和老一代.对于每个方法调用,JVM都会创建一个包含局部变量的新堆栈框架.将它们保持为堆栈有助于检索最近的堆栈帧,即方法返回时轻松调用方方法的变量集.
9、什么是克隆?浅克隆和深克隆有什么区别?
克隆意味着创建对象的副本或创建重复的对象-它们的状态应该相同.一个对象可能由其他几个对象组成.浅克隆将创建一个新对象,并将其字段值分配给新对象的相应字段.由于字段仅包含驻留在堆中的对象的引用,因此新对象的字段也指向相同的组件实例.浅克隆虽然速度很快,但缺点是,如果更改了任何组件对象,它也会在克隆的对象中反映出来,因为这两个对象都持有相同对象的引用.
另一方面,深度克隆不会复制字段中的引用,它还会创建组件对象的副本.与深度克隆中一样,所有组件对象都被克隆,这比较慢,但是会创建实际对象的真实副本.
10、Java 8中的Optional
Optional的是一个对象容器,其中可能不包含非null值.如果有值,则isPresent()将返回true,而get()将返回该值.提供了依赖于包含值的可用性的补充方法,例如orElse()方法(如果不存在值,则返回默认值)和ifPresent()(如果存在值,则执行代码块).
11、Java中,何时使用double型而不是float型?
double类型和float类型都用于表示Java中的浮点数.但是,在某些情况下,双精度型更好,在某些情况下,浮型更好.
如果需要更精确的结果,则双精度型优于浮型.双精度型的精度最高为15到16个小数点,而浮点型的精度仅为6到7个十进制数字.
双重类型优于浮动类型的另一个原因是它具有更大的范围.它对符号使用1位,对指数使用11位,对尾数使用52位,而float型仅对符号使用1位,对指数使用8位,对尾数使用23位.
12、Java中使用多线程的最佳实践
给线程命名,这样可以帮助调试。
最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
如果可以,更偏向于使用 volatile 而不是 synchronized。
使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。
由于不能同时执行锁中的任何代码,因此应将锁定范围最小化,这会降低应用程序性能.
并发集合应该比同步集合更可取,因为它们可以提供更多的可伸缩性和性能.
应该使用不可变的类(例如String,Integer等)以及其他包装器类,因为它们可以简化并发代码的编写.这是因为无需担心状态.
对于可伸缩的Java应用程序来说,线程池是更好的选择,因为线程创建非常昂贵.
不应使用类或实例变量,而应尽可能使用局部变量.
实施生产者使用者设计模式的最佳方法是使用BlockingQueue.这是非常重要的,因为许多并发问题都是基于生产者消费者设计的.
有许多同步实用程序,例如CyclicBarrier,CountDownLatch和Semaphore都应使用,而不是等待和通知.
13、Java中使用Collections时的最佳实践
在使用集合之前,需要根据需要解决的问题选择合适的集合.
应该根据需要使用Java Collections Framework提供的Arrays和Collections实用程序类,因为它们提供了许多有用的方法来搜索,排序和修改集合中的元素
集合的初始容量始终由几乎所有具体集合类中都包含的重载构造函数指定.
在检查集合是否为空时,应优先使用isEmpty()方法而不是size()方法.即使这两种方法在性能上存在差异,也可以这样做以提高此代码的可读性.
如果方法返回一个集合,那么如果集合中没有元素,则该方法不应返回null.相反,它应该返回一个空集合.
Java 8中的每个集合中都有一个流方法,该方法返回元素流.这意味着可以使用Stream API轻松地执行聚合功能.
与其使用经典的for循环来迭代列表集合,不如使用迭代器.这是因为如果在循环内部更改了for循环变量,则可能导致错误.
14、Java中的volatile关键字
使用volatile关键字可以使类成为线程安全的.这意味着多个线程可以同时使用类实例或方法,而不会出现任何问题.
在特定变量上使用volatile关键字的含义如下:
具有volatile关键字的变量的值永远不会在本地缓存线程.这意味着对变量的所有读和写操作都保存在主存储器中.
对该变量的访问就好像它被包含在一个同步块中并且本身已同步一样.
volatile关键字的示例如下:
class Shared
{
static volatile int value = 15;
}
在上面的示例中,如果一个线程进行了任何更改,那么它们也将使用volatile关键字反映在其他线程中.
15、volatile 和 synchronized之间的区别
易失性和同步之间的一些区别如下:
特性 | volatile | synchronized之间的区别 |
---|---|---|
是否可以使用null? | 是 | 没有 |
变量类型 | 对象变量或原始变量 | 对象变量 |
何时同步? | 当访问volatile变量时 | 显式进入或退出同步块时. |
所有缓存的变量是否在访问时同步? | 从Java 5开始,这是正确的. | 是 |
可以用于将多个操作合并为一个原子操作吗? | 在Java 5之前这是不可能的. | 是 |
16、用户线程和守护线程之间有什么区别?
当我们在Java程序中创建线程时,它被称为用户线程.守护程序线程在后台运行,并且不会阻止JVM终止.当没有用户线程在运行时,JVM会关闭程序并退出.从守护程序线程创建的子线程也是守护程序线程.
17、什么是多线程中的上下文切换?
上下文切换是存储和恢复CPU状态的过程,以便可以在以后的某个时间点从同一点恢复线程执行.上下文切换是多任务操作系统的基本功能,并且支持多线程环境.
18、抢占式调度和时间分片有什么区别?
在抢占式调度下,最高优先级的任务会一直执行,直到进入等待状态或死机状态或存在更高优先级的任务为止.在时间分片下,任务将执行预定义的时间片,然后重新进入就绪任务池.然后,调度程序根据优先级和其他因素确定下一步应执行的任务.
19、是否可以启动一个线程两次?
不,不可能两次启动一个线程.如果这样做,它将引发异常.
20、我们可以调用run()方法而不是start()吗?
是的,但是它将不能用作线程,而可以用作普通对象,因此在线程之间不会进行上下文切换.
21、我们如何在特定时间内暂停执行线程?
我们可以使用Thread类的sleep()方法将Thread的执行暂停一定时间.请注意,这不会在特定时间内停止线程的处理,一旦线程从睡眠中醒来,其状态将更改为可运行,并根据线程调度执行该线程.
22、什么是线程调度程序和时间切片?
是一种操作系统服务,它将CPU时间分配给可用的可运行线程.创建并启动线程后,其执行取决于Thread Scheduler的实现.
是将可用CPU时间划分为可用可运行线程的过程.可以根据线程优先级为线程分配CPU时间,或者等待更长时间的线程将在获得CPU时间时获得更高的优先级.线程调度不能由Java控制,因此始终最好从应用程序本身控制线程调度.
23、线程如何相互通信?
当线程共享资源时,线程之间的通信对于协调其工作很重要.对象类的wait(),notify()和notifyAll()方法允许线程就资源的锁定状态进行通信.
24、如何在Java中实现线程安全?
有几种方法可以在Java中实现线程安全:synchronization, atomic concurrent classes, implementing concurrent Lock interface,使用volatile关键字,使用不可变类和Thread安全类.
25、如何在Java中创建守护程序线程?
线程类setDaemon(true)可用于在Java中创建守护程序线程.我们需要在调用start()方法之前调用此方法,否则它将引发IllegalThreadStateException.
26、什么是ThreadLocal?
Java ThreadLocal用于创建线程局部变量.我们知道对象的所有线程都共享它的变量,因此,如果变量不是线程安全的,则可以使用同步,但是如果要避免同步,则可以使用ThreadLocal变量.
每个线程都有自己的ThreadLocal变量,他们可以使用它的get()和set()方法获取默认值或将其本地值更改为Thread.ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段.
27、什么是Java Thread Dump,我们如何获取程序的Java Thread Dump?
Thread Dump是JVM中所有活动线程的列表,Thread Dump对于分析应用程序中的瓶颈和分析死锁情况非常有帮助.我们可以使用多种方法来Dump Thread –使用Profiler,Kill -3命令,jstack工具等.我更喜欢使用jstack工具来进行 Dump Thread,因为它易于使用并且随JDK安装一起提供.由于它是基于终端的工具,因此我们可以创建脚本以定期Dump Thread,以供日后分析.
28、什么是线程池?我们如何在Java中创建线程池?
线程池管理工作线程池;它包含一个使任务等待执行的队列.
线程池管理可运行线程的集合,工作线程从队列中执行可运行线程.
java.util.concurrent.Executors提供java.util.concurrent.Executor接口的实现,以在Java中创建线程池.线程池示例程序显示了如何在Java中创建和使用线程池.或阅读ScheduledThreadPoolExecutor示例以了解如何在特定延迟后安排任务.
29、 构造器(constructor)是否可被重写(override)?
构造器不能被继承,因此不能被重写,但可以被重载。
30、 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
31、抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
32、Java 中的final关键字有哪些用法?
答:(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
33、Error 和Exception 有什么区别?
答:Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
34、Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
答:Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java 中,每个异常都是一个对象,它是Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。Java 的异常处理是通过5 个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throw)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理;try用来指定一块预防所有“异常”的程序;catch 子句紧跟在try块后面,用来指定你想要捕捉的“异常”的类型;throw 语句用来明确地抛出一个“异常”;throws用来标明一个成员函数可能抛出的各种“异常”;finally 为确保一段代码不管发生什么“异常”都被执行一段代码;可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try 语句,“异常”的框架就放到栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种“异常”进行处理,栈就会展开,直到遇到有处理这种“异常”的try 语句。
35、final, finally, finalize 的区别?
答:final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final 的方法也同样只能使用,不能在子类中被重写。finally:通常放在try…catch的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。
36、什么是TreeMap
TreeMap 是一个,它是通过红黑树实现的。
TreeMap基于。该映射根据,或者根据,具体取决于使用的构造方法。
TreeMap是线程的。
37、Java语言采用何种编码方案?有何特点?
Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
38、Java 中你怎样唤醒一个阻塞的线程?
首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
39、多线程同步有哪几种方法?
Synchronized关键字,Lock锁实现,分布式锁等。
40、什么是“依赖注入”和“控制反转”?为什么有人使用?
控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好~
依赖注入(DI) 在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方~
41、ArrayList 和 LinkedList 的区别是什么?
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
42、Array与ArrayList有什么不一样?
Array与ArrayList都是用来存储数据的集合。ArrayList底层是使用数组实现的,但是arrayList对数组进行了封装和功能扩展,拥有许多原生数组没有的一些功能。我们可以理解成ArrayList是Array的一个升级版。
实例化数组后,能不能改变数组长度呢?
不能,数组一旦实例化,它的长度就是固定的
43、ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
JDK1.7的 ConcurrentHashMap 底层采用 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
在JDK1.7的时候,ConcurrentHashMap(分段锁对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
HashTable
JDK1.7的ConcurrentHashMap
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点)
ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。
44、Java 中 LinkedHashMap 和 PriorityQueue 的区别是什么?
PriorityQueue 保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
45、成员变量与局部变量的区别有那些?
从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰;成员变量和局部变量都能被final所修饰;
从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存
从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被final修饰但没有被static修饰的成员变量必须显示地赋值);而局部变量则不会自动赋值。
46、JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
如果对象的大小大于Eden的二分之一会直接分配在old,如果old也分配不下,会做一次majorGC,如果小于eden的一半但是没有足够的空间,就进行minorgc也就是新生代GC。
minor gc后,survivor仍然放不下,则放到老年代
动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
47、32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。64 位 JVM允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
48、有没有可能两个不相等的对象有有相同的 hashcode?
有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。
49、synchronized 和 Lock 有什么区别?
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
50、什么是Vector
Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多
ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。
51、什么是可重入锁(ReentrantLock)?
举例来说明锁的可重入性
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
synchronized、ReentrantLock都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
52、你有哪些手段来排查 OOM 的问题?
增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域
使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用
53、线程池有什么优点?
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
54、String 属于基础的数据类型吗?
String 不属于基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 属于对象。
55、线程的 sleep()方法和 yield()方法有什么区别?
sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
56、线程池中 submit() 和 execute() 方法有什么区别?
相同点就是都可以开启线程执行池中的任务。
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
57、什么是双亲委派机制?
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
58、列举一些你知道的打破双亲委派机制的例子。为什么要打破?
1、 JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
2、 Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
3、 OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
4、 JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。
59、什么是上下文切换?
1、 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
2、 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
3、 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
4、 Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
60、Xml的java解析有几种方式?
Dom解析:一次性加载整个文档,生成树形结构。在生成的文档对象中,可以对节点进行增删改查的操作。当xml文本当较小的时候,可以使用dom解析。
Sax解析:基于事件的解析方式,解析速度比较快,解析的文档大小理论上是没有限制的。
还有一些开源的技术可以解析xml,dom4j或者jdom
61、JVM 类加载机制
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。
加载
加载是类加载过程中的一个阶段, 这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080, 将 v 赋值为 8080 的 put static 指令是程序被编译后, 存放于类构造器方法之中。
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的
public static int v = 8080;
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080, 将 v 赋值为 8080 的 put static 指令是程序被编译后, 存放于类构造器方法之中。但是注意如果声明为:
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。
符号引用
符号引用与虚拟机实现的布局无关, 引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
类构造器
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕, 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
注意以下几种情况不会执行类初始化:
1、 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2、 定义对象数组,不会触发该类的初始化。
3、 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4、 通过类名获取 Class 对象,不会触发类的初始化。
5、 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6、 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
62、熔断器hystrix。限流的几种方式 ,网关
方式:信号量,线程池。
信号量和线程池区别
线程池:满了到队列,队列满了 就拒绝限流了
信号量:配置一个数字-信号量,请求时,计数器会+1,请求完毕计数器会-1,当并发数达到信号量配置,限流
区别是,线程池时异步的,信号量是同步的,线程池会有一个队列缓冲,信号量的话并发高会直接拒绝
63、ribbon的超时时间和hystrix的时间,谁要大一点?
hystrix应该大一点
64、分布式锁的实现有哪些
Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add成功,也就意味着线程得到了锁。
Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。
基于Redission的实现
65、线程池一般根据什么样的规则来设置是最佳的?
最佳线程数目 = (线程等待时间与线程占用 CPU 时间之比 + 1)* CPU数目
66、怎么防止线程死锁?
首先要保证加锁的顺序性;其次超时释放锁并尝试重新获取锁。
67、ThreadLocal 了解吗?都用在什么场景?
ThreadLocal位于JDK的java.lang核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。ThreadLocal的英文字面意思为“本地线程”,实际上ThreadLocal代表的是线程的本地变量
ThreadLocal 提供了一种方式,让在多线程环境下,每个线程都可以拥有自己独特的数据,并且可以在整个线程执行过程中,从上而下的传递。
适用场景:1、变量在线程间隔离,而在方法或类间共享的场景、2、跨函数传递数据。
68、悲观锁对多线程的竞争性有什么影响?
悲观锁竞争激烈的情况下会升级,即锁粗化,顺序是:偏向锁 → 轻量级锁 → 重量级锁。
69、如何停止线程池?
ExecutorService 接口中有两种停止的方法:
void shutdown(); 有序的停止线程池,继续执行已提交的任务,但不再接受新的任务;
List<Runnable> shutdownNow(); 立即停止线程池,立即停止所有正在执行的任务,暂停所有等待执行的任务,并返回等待执行的任务列表。
70、如何停止一个正在运行的线程?
在java中有以下3种方法可以终止正在运行的线程:
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
使用interrupt方法中断线程。