[1]. String为什么是线程安全的?
在Java中,存在一个存储字符串对象的特殊内存区域字符串常量池。当创建一个字符串时,如果该字符串已经存在于字符串常量池中,直接返回这个字符串的引用;如果不存在,将该字符串添加到字符串常量池中,并返回新创建的字符串的引用。
String类内部使用一个final修饰的字符数组来存储字符序列。final关键字确保了该数组在String对象生命周期内不会被重新赋值。
主要还是String的不可变性,一旦String对象被创建,其内部的字符序列就不能被改变。这一特性使得String可以在多个线程之间安全地共享,而不需要额外的同步机制。
[2]. StringBuilder和StringBuffer的区别
StringBuffer所有的公开方法都被synchronized 修饰,从而保证了多线程环境下的数据一致性,是线程安全 的,而StringBuilder并没有使用synchronized修饰其方法,是线程不安全的。
StringBuffer的toString方法使用缓存区的toStringCache 的值来构造一个字符串,而StringBuilder的toString方法是通过复制字符数组来构造一个字符串。
StringBuffer适用于需要在多线程环境 中安全地构建字符串的场景,而StringBuilder适用于单线程环境或不需要线程安全的字符串构建场景。
[3]. Hashcode()和equals()为什么要重写?
equals() 方法用于比较两个对象是否"相等"。Java中在Object类中的默认实现是基于对象的内存地址来比较的,如果两个对象不是同一个实例,即使它们的内容完全相同,equals() 方法也会返回 false。
hashCode() 方法 返回一个哈希码(整数),用于确定对象在哈希表中的位置。两个对象的哈希码相同是两个对象根据equals()方法也相等的必要不充分条件。也就是说,如果两个对象的哈希码相同,不能直接断定它们是相等的(因为可能存在哈希冲突),但如果它们相等,那么它们的哈希码一定相同。
Java中在Object类中的默认实现是基于对象的内存地址来计算的。如果不重写hashCode(),即使两个对象在逻辑上相等,它们也可能有不同的哈希码,这将导致哈希表无法正确识别它们,从而可能出现查找失败 ,重复插入等问题。
[4]. synchronized和Reentranlock的区别
synchronized是Java语言内置的一个关键字 ,可用来修饰普通方法 、静态方法 和代码块 ,而ReentrantLock基于抽象类AQS(AbstractQueuedSynchronizer)实现的一个类 ,只能用于代码块。
synchronized会自动 加锁和释放锁,而ReentrantLock需要手动加锁和释放锁。
synchronized是非公平锁 ,而ReentrantLock 默认 为非公平锁,也可以手动指定为公平锁。
synchronized不能响应中断 ,如果发生了死锁会一直等待下去,而使用 ReentrantLock可以响应中断并释放锁,从而解决死锁的问题。
[5]. 数据库中有哪些锁
按锁的粒度划分:表级锁、行级锁、页级锁
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率高,并发度低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。
页级锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般。
按锁对资源的访问权限划分:共享锁、排他锁
共享锁(读锁):当一个事务已经获取了数据项的共享锁后,其他事务可以继续对该数据项加共享锁,但是不能加排他锁。
排他锁(写锁):当一个事务对一个数据项加上排他锁后,其他事务无法再对该数据项加上任何类型的锁。
[6]. MySQL数据库的索引底层实现
MySQL的索引使用B+树作为底层的数据结构,举例说明一下:
比如当我们执行
select * from xxx where name="x"
这条SQL语句的时候,数据库引擎会检查name字段有没有索引,如果没有索引数据库引擎会做全表扫描 ,这样的话效率会比较低。如果有索引,数据库引擎会向内核申请从磁盘上读取索引文件到内存,一个节点接着一个节点在内存上构建B+树,每个节点对应一次磁盘IO 。非叶子节点只存储关键字key,所有的key和data都存储在叶子节点,所以用B+树会以最少的磁盘IO构建出索引树。另外,在搜索的时候是以二分的方式进行搜索的,时间复杂度为O(logn)。
[7]. B+树和B树的区别
B+树只有叶子节点 存储数据,非叶子节点 仅存储索引 ,叶子节点间通过指针连接,B树的叶子结点和非叶子节点都会存储数据。
B+树所有数据记录都存储在叶子节点上,且叶子节点同时还维护了一条双向链表 ,提高了范围查询的效率,而B树需要在各个节点上逐个查找,范围查找效率较低。
B+树适用于范围查询和顺序访问的场景,如数据库索引 。B树适用于需要随机访问的场景,如文件系统索引。
[8]. 常见的解决哈希冲突的方式
哈希冲突的解决方法主要分为两大类:开放地址法 和链地址法。
开放地址法又分为3种:
线性探测法 :当发生哈希冲突时,就线性地往后寻找下一个可用的位置,直到找到一个空位置为止。即,如果hash(key)
的位置被占用,则尝试hash(key) + 1、hash(key) + 2...
直到找到一个空位置。
二次探测法 :当发生哈希冲突时,通过二次方程来寻找下一个可用的位置,以减小探测的步长。即,如果hash(key)
的位置被占用,则尝试hash(key) + 1^2、hash(key) - 1^2、hash(key) + 2^2...
。
双重散列法 :使用两个哈希函数,首先使用第一个哈希函数计算出一个初始哈希值,如果该位置已经被占用,就通过第二个哈希函数来计算下一个位置。即,如果hash1(key)
的位置被占用,则尝试hash1(key) + hash2(key)、hash1(key) + 2 * hash2(key)...
。
链地址法:
将哈希表的每个槽设置为一个链表,当发生哈希冲突时,将具有相同哈希值的元素存储在同一个链表中。
[9]. HashMap线程不安全的例子
当两个线程同时进行插入 操作时,假设它们都要插入到同一个数组位置,并且该位置没有元素,那么它们都会认为该位置可以插入元素,最终就会导致其中一个线程的元素被覆盖 掉。类似地,删除 操作也可能因相同原因导致数据被覆盖掉。
[10]. 常用的垃圾回收算法
标记-清除算法:由标记阶段和清除阶段构成。标记阶段遍历所有可达对象,将其标记为"活动";清除阶段遍历内存空间,回收那些未被标记为"活动"的对象。
复制算法:将内存分为两半,每次只使用其中一半来分配对象。当这一半内存用尽时,复制当前这一半内存中所有存活的对象到另一半空闲内存中,并回收当前使用的一半内存。
标记-整理算法:在标记-清除算法基础上,不直接清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
分代收集算法 :一般情况下将堆区划分为老年代 (Tenured Generation)和新生代(Young Generation),新生代存活率低,使用复制算法。而老年代对象存活率高,使用标记-清除或者标记-整理算法。
[11]. Java中常用的垃圾收集器
根据其工作方式可大致分类为串行 (Serial)、并行 (Parallel)和并发(Concurrent)三种类型。
串行垃圾收集器指的是Serial GC,并行垃圾收集器指的是Parallel GC,并发垃圾收集器有4种,分别是CMS GC、G1 GC、ZGC和Shenandoah GC。
Serial GC:单线程的收集器,只能在暂停所有应用线程的情况下进行垃圾回收。适用于单核处理器或小型应用。
Parallel GC:Serial GC的多线程版本。适用于多核处理器,追求高吞吐量的应用。
CMS(Concurrent Mark Sweep) GC:以获取最短回收停顿时间为目标的收集器,适用于Web服务器这种对响应时间有较高要求的应用。
G1(Garbage-First) GC:面向服务端应用的垃圾收集器。将堆内存划分为多个区域,并根据垃圾分布情况优先收集垃圾最多的区域。适用于追求高吞吐量和低停顿时间的应用。
ZGC:低延迟的垃圾回收器,旨在实现极低的停顿时间(不超过10ms)。适用于对低延迟有较高要求的应用程序。
Shenandoah GC:另一种低延迟的垃圾收集器,与ZGC类似。
[12]. Java深拷贝和浅拷贝的区别
在拷贝一个对象时,对于对象中的基本数据类型 ,深拷贝和浅拷贝没有区别 。但是对于对象中的引用数据类型 来说,浅拷贝只是将引用数据类型的地址值复制一份给新对象,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值会随之改变 ;而深拷贝会在新的内存空间里创建一个与原始对象中引用数据类型内容完全相同的新对象,新旧对象不共享内存,相互独立,修改其中一个对象的值,不会影响另一个对象。