面试题总结

文章目录

垃圾回收

1. JVM垃圾回收中,如何判断一个对象需要回收

判断标准:**这个对象有没有被其他对象所引用。**这个对象所占用JVM的内存空间,应该被释放,对象也应该销毁。

判定对象是否是垃圾的算法:
– 引用计数算法:判断对象的引用次数;
每个对象实例都有一个引用计数器,只要被引用了,就会加一。如果引用去掉,就减1。存在问题:循环引用的情况就会导致内存泄漏。(这里解释下内存泄漏)
– 可达性分析算法:通过判断对象的引用链条是否可达来决定对象是否被回收;
在这里插入图片描述
GCroot:
– 虚拟机栈中引用的对象;
– 方法区中常量所引用的对象;
– 方法区中类静态属性所引用的对象;
– 本地方法中JNI,也就是Native引用对象;
– 活跃线程的引用对象;

2. 谈谈你所了解的垃圾回收算法有哪些?

– 标记清除算法(Mark and Sweep)
首先要做标记,再做清除。
首先通过根对象进行扫描,把存活的对象(与gcroot连接的对象)打一个标记。再在堆内存中,从头到尾进行一个遍历,回收不可达的对象的内存。
缺点在于:空间中的碎片太多,可能会导致,以后程序在运行中,如果分配的对象比较大时,可能无法找到连续的内存空间存储。这样可能去触发新的垃圾回收。
在这里插入图片描述
– 复制(Copy)
将内存按照容量划成两个部分,每次只使用其中一块:对象创建时首先在一块上面创建,当你这块内存用完的时候,会将这块内存上还存活的对象复制到另外一块上面。再把已经用过的内存空间给清理掉。
碎片没有了,按照顺序分配的,简单高效。适合用于对象存活率低的场景,年轻代。年轻代适合使用这种算法。因为年轻代的存活率低,比较适合于复制。
缺点:能够利用的内存空间减少,利用率低。且不适合存活率多,当存活对象比较多,每次做复制时,它要做对象的迁移,还要做顺序的分配,故老年代不适合这种算法。
在这里插入图片描述

– 标记-整理算法
对比第一种标记清除,首先都是从根对象进行扫描,把存活的对象标记好。第一种清除并不会移动存活的对象,仅仅只是把不存活的对象干掉。故整理这个部分是需要移动所有的对象的。并且按照内存地址的次序进行排列。
优点:解决了第一种的碎片,避免了内存的不连续性。且并不需要设置两个内存的互换,利用率一定比复制要高。适用于存活率比较高的场景(老年代)。
内存整理比较耗时。
– 分代收集算法(组合拳)
内存中不同的代,采用不同的垃圾回收算法。按照对象的生命周期,可以划分为不同的区域,不同的区域采用不同的垃圾回收算法。目的是为了提高JVM的回收效率。为什么要采用这种回收机制?
大部分对象都是创建在young区,生命周期比较短。转眼间都会被垃圾所回收,这种方式应该采用复制算法,只要把存活的对象进行复制,效率比较高。因为存货的东西已经不多了。
old代可以采用标记清除/标记整理。
每次回收后,对象都有一个年龄的概念,做一次后年龄+1。默认情况下,当转换或者收集的次数到一定的年龄后,它会被移到老年代里面去。
年轻代采用:8:1:1存放,from/to每次仅仅会使用一个。
Stop-the-world:SPark执行设置的内存不合理时,看到jc的时间非常长时,就要考虑是不是没有及时被回收。
在这里插入图片描述

3. 垃圾回收器的分类

垃圾收集器就是对垃圾回收算法的实现。
垃圾收集器分类:
串行搜集器;(Serial Collector)
只有一个线程:JVM在分配内存时,发现内存不够用,就会暂停应用程序的执行。
这就是Stop the world。

并行搜集器(Parallel Collector);

并发搜集器(Concurrent Collector);

并行:多条垃圾收集线程并行工作,用户线程处于等待状态。
并发:用户线程和垃圾收集线程同时执行,不一定是并行,有可能是交替执行的。
垃圾收集线程在执行时不会停顿用户程序的执行,适合用于响应时间有要求的场景,比如web。

决定了停顿的时间和吞吐量的问题。

吞吐量:花在垃圾收集的时间和花在应用的时间的一个占比。
串行:
serial:新生代 复制算法;
serial old:老年代 标记整理
并行:
parNew:新生代 复制
parallel Seavenge: 新生代 复制
Parallel old:老年代 标记整理

并发:
Concurrent Mark Sweep CMS
老年代 标记清除算法;

并行的追求吞吐量优先,并发的追求响应时间优先。

在这里插入图片描述
不同代可以采用不同的收集器来处理。

在这里插入图片描述
停顿时间超过了1s,就选择并行的。响应时间越短,就采用并发。

4. 解释下内存泄露,怎么解决?

如果您忘记释放对象,则将无法重用该内存。该内存将被声明但未被使用。这种情况称为内存泄漏。解决内存泄漏的方法就是自动回收未使用的内存,从而完全消除人为错误的可能性。这种自动化称为垃圾回收(GC)。

5. 什么可以作为GC root?**

– 虚拟机栈中引用的对象;
– 方法区中常量所引用的对象;
– 方法区中类静态属性所引用的对象;
– 本地方法中JNI,也就是Native引用对象;
– 活跃线程的引用对象;

6. 如果对象大部分都是存活的,少部分需要清除,用什么算法

标记清除算法。标记清除的缺点主要是对没有标记的对象进行清理时容易产生内存碎片,所以当对象大部分是存活时,可以考虑使用标记清除算法。

7. 说一下你知道的垃圾收集算法和垃圾收集器

垃圾收集器(垃圾回收的具体体现):

1.新生代:Serial、ParNew、parallel Scavenge
2.老年代:Serail old、ParNew old、CMS

1.新生代回收器的详细介绍:

a.Serial:它是一个单线程的垃圾回收器,单线程的意义是:只会使用一个CPU或者一条垃圾收集线程去完成垃圾收集工作。而在它进行垃圾收集工作的时候,其他线程必须暂停,直到垃圾收集结束。

b.ParNew:它是serail的多线程版本,基本操作和Serial一样。该收集器一般和CMS搭配工作。

c.parallel Scavenge:此收集器的目的是达到一个可控制的吞吐量。

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾回收的时间)

d.GC自适应策略:JVM会根据当前系统运行情况收集性能监控情况,动态调整这些参数以提供最适合的停顿时间或者吞吐量。

=======================================
老年代垃圾收集器:
1.Serial Old:是Serail 的老版本,也是单线程收集器,该收集器主要是给Client模式下的虚拟机使用的。
Note:分代收集算法:新生代采用复制算法,并暂停所有用户线程。 老年代采用标记整理法,并暂停所有用户线程。
2.Parallel Old:是Parallel的老版本,使用多线程和标记整理算法进行垃圾回收。
3.CMS:是一种以获取最短回收停顿时间为目标的收集器。该收集器是基于"标记清除"算法实现的。

=======================================
新生代和老年代垃圾回收器:
G1
G1收集器所具备的特点有哪些:
1.并发和并行:使用多线程来缩短暂停时间,G1收集器通过并行的方式让Java继续执行。
2.分代收集:
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。G1收集器之所以可以有计划地避免在整个Java堆中进行全区域的垃圾收据,是因为G1收集器跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。即Grabage-First。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是通过Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set。在新建对象时,JVM会将相关的引用信息记录到被引用对象所属的Region的Remembered Set中。当进行回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对堆进行扫描也不会有遗漏。

3.空间整合:采用标记整理算法实现
4.可预测的停顿:建议可预测的停顿时间模型,让使用者明确在y一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒,达到了实时的垃圾收集器。

G1垃圾收集器的收集阶段分为以下几步:
1、初始标记(只是标记一下GC Roots能直接关联到的对象,并修改可以得Region中创建新对象,这阶段需要停顿线程,但耗时很短)
2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)
3、最终标记(修正在并发标记期间因月洪湖程序继续运行而导致标记产生变动的那一部分标记记录)
4、筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)

Java

1. Java类的加载过程

Java的类加载过程是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程:JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程;

类加载分为三个步骤:
加载:把class字节码文件通过类加载器装载入内存中;
连接:为类变量分配内存,并且赋予初值;
初始化:对类变量初始化,执行类构造器的过程;

加载:把字节码读取到内存,尝试会创建一个CLASS对象,尝试可能会失败。

连接:

  1. 验证
  2. 准备:开始在方法区分配空间。

2. 堆内存是怎么分配的

3. java中强引用,软引用,弱引用,虚引用有什么用

4. 谈一下String、StringBuffer、StringBuilder。

– String:

  • 不可变:String类中使用final关键字字符数组保存字符串,private final char value[],故String对象是不可变的。
  • 线程安全:String中的对象是不可变的,即可以理解为常量,线程安全;
  • 性能:每次对String类型进行改变时,都会生成一个新的String对象,然后将指针指向新的String对象;

– StringBuffer和StringBuilder:

  • 可变性:继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串char[] value,但是没有用final关键字修饰,所以这两种对象都是可变的。
  • 线程安全性:AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity.append.insert.indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法加同步锁,故是非线程安全的。
  • 性能:StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StringBuilder相比使用StringBuffer能获得10%-15%的提升,但要冒多线程不安全的风险。

– 最后总结:

  1. 操作少量数据时,用String;
  2. 单线程操作字符串缓冲区下操作大量数据时,用StringBuilder;
  3. 多线程操作字符串缓冲区下操作大量数据时,用StringBuffer;

5. 说下同步锁;

6. 说下final关键字;

  1. final主要时作用于变量、方法、类这三个地方;
  2. 对于final修饰的变量,如果是基本数据类型变量,在数值初始化后便不能更改;如果是引用类型的变量,则在初始化后不能再让其指向另一个对象。
  3. 对于final修饰的类,表明这个类不能被继承。final类中的所有成员方法都会被隐式的指定为final方法。
  4. 对于final修饰的方法,有两个原因,其一是为了防止任何继承修改方法的含义,把方法锁定,其二是因为效率。

7. 说下接口和抽象类的区别。

  1. 接口的方法默认是public,所有方法在接口中不能有实现(在java8后接口方法可以用默认实现),抽象类可以有非抽象的方法;
  2. 接口的实例变量默认是final类型,而抽象类则不一定;
  3. 一个类可以实现多个接口,但最多只能实现一个抽象类;
  4. 一个类实现接口的话要实现接口的所有方法,而抽象类则不一定;
  5. 接口不能用new来实例化,但是可以声明,但是必须引用一个实现该接口的对象,可以通过接口引用指向一个对象。 即一个接口类型的引用指向了一个实现给接口的对象,这是java中的一种多态现象 ,java中的接口不能被实例化,但是可以通过接口引用指向一个对象,这样通过接口来调用方法可以屏蔽掉具体的方法的实现,这是在JAVA编程中经常用到的接口回调,也就是经常说的面向接口的编程。
  6. 从设计层面上来说,抽象是对类的抽象,是一种模板设计。接口是行为的抽象,是一种行为规范。

8. 红黑树是个啥

红黑树是自平衡的二叉查找树,在进行插入和删除等可能会破坏树的平衡的操作时,需要重新自处理达到平衡状态。

平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构;平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;

任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

9.equals 和==

==: 判断两个字符串在内存中首地址是否相同,即 判断是否是同一个字符串对象;
equals():比较存储在两个字符串对象中的 内容是否一致;

网络

1. 网络了解吗,说说输入网址按下回车后的过程;

总体来说分为以下几个过程:

  1. 首先是DNS 解析,将域名解析成 IP 地址,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。
  2. 向服务器发送 HTTP 请求,此时发送端已经有了页面的ip地址,需要做的就是向服务器发送http请求。发送http请求包括三个阶段:
    – TCP三次握手–在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息;
    – http 请求响应信息:TCP三次握手结束后,开始发送http请求报文,并由服务器处理请求并返回 HTTP 报文。
    – 浏览器解析渲染页面:浏览器拿到响应文本 HTML 后,开始解析并渲染页面;
  3. 断开连接:当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手:
    – 第一次挥手:浏览器发起,发送给服务器,我请求报文完了,你准备关闭吧;
    – 第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧;
    – 第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧;
    – 第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧;

2. Http和https的区别是什么,https的数据传输过程?

HTTP协议是超文本传输协议的缩写,是基于TCP/IP的关于数据如何在万维网中如何通信的协议。

设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。
HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS
区别:

  1. Https需要到CA 申请证书,Http不需要
  2. Https 密文传输,Http明文传输
  3. 连接方式不同,Https默认使用的是 443端口,Http使用 80端口
  4. Htttps = http + SSL(加密+认证+完整性保护),较Http安全;

Https 数据传输过程:
(1). 浏览器将支持的加密算法信息发给服务器
(2). 服务器选择一套浏览器支持的加密算法,以证书的形式回发给浏览器
(3). 浏览器验证 证书的合法性,并结合证书 公钥 加密信息发给服务器
(4). 服务器使用 私钥解密信息,验证哈希,加密响应消息回发给 浏览器
(5). 浏览器解密响应信息,并对消息进行验证,之后进行加密交互数据

3. GET请求和POST请求的区别

首先直观的区别就是GET把参数包含在URL中,POST通过request body传递参数;

GET和POST是什么?HTTP协议中的两种发送请求的方法

HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议

HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。

交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET)

GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。 虽然GET可以带request body,也不能保证一定能被接收到

区别:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

4. TCP/IP的三次握手和四次挥手

握手流程 :
1. 第一次握手,建立连接时,客户端发送SYN包 ( syn=j )到服务器,并进入SYN_SEND状态,等待服务器确认。
2. 第二次握手:服务器收到SYN 包,必须确认客户的SYN (ack=j+1),同时自己也发送一个SYN包 (syn=k),即 SYN+ACK包,此时服务器进入 SYN_RECV 状态;
3. 第三次握手:客户端收到服务器的SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1), 此包发送完毕,客户端和服务器进入ESTAB -LISTEN 状态,完成三次握手。

问题、隐患:
首次握手的隐患----SYN超时
针对SYN Flood的防护措施:
1. SYN队列满后,通过tcp_syncookies 参数回发SYN Cookie
2. 若为正常连接则Client会回发 SYN Cookie, 直接建立连接

5. 为什么需要四次握手才能断开连接:

因为全双工,发送方和接收都需要FIN报文和ACK 报文

设计模式

1. double check的singleton

单机版:

public class SingletonDemo{
	private static SingletonDemo instance = null;
	private SingletonDemo(){
		System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
	}
	public static SingletonDemo getInstance(){
		if(instance == null){
			instance = new SingletonDemo();
		}
		return instance;
	}
	public static void main(String[] args){
		System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
	}
}

double:用10个线程来对它进行调用

public class SingletonDemo{
	private static volatile SingletonDemo instance = null;
	private SingletonDemo(){
		System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
	}
	// DCL(Double Check Lock双端检锁机制)
	public static SingletonDemo getInstance(){
		if(instance == null){
			sychronized(SingletonDemo.class){
				if(instance == null){
					instance = new SingletonDemo();
					
				}
			}
			instance = new SingletonDemo();
		}
		return instance;
	}
	public static void main(String[] args){
		System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
	}
}

还有指令重排序的存在,需要加volatile来禁止指令重排;

尚未实例化时,存在两次检查的流程,第一次检查如果发现该实例已经存在就可以直接返回,反正则加类锁并进行第二次检查,原因在于可能出现多个线程同时通过了第一次检查,此时必须通过锁机制实现真正实例化时的排他性,保证只有一个线程成功抢占到锁并执行。此举即保证了线程安全,又将性能折损明显降低了,不失为比较理想的做法。

2. 解释下单例模式,并说明单例模式适合于什么场景?

排序

1. 快排和归并的异同;

快排的话是三个步骤:先确定分界点;再调整区间,让第一个区间的值都小于等于x,第二个区间的值都大于等于x,最后递归处理左右两端。

归并的话是:先确定分界点mid;再递归的处理左右两端;最后将两个有序数组归并为一个;

2. 归并的最坏时间复杂度是多少,且什么时候会出现最坏的时间复杂度;

最坏、最佳、平均情况下归并排序时间复杂度均为o(nlogn),从合并过程中可以看出合并排序稳定。

3. 快排的最坏时间复杂度是多少,且什么时候会出现最坏的时间复杂度;

最坏情况是n2,但通过随机算法可以避免最坏情况。
当划分产生的两个子问题分别包含 n-1 和 0 个元素时,最坏情况发生

4. 快排遇到了逆序排序的问题,该怎样进行优化?

随机算法:只需要在排序前随机去一个元素和末端元素交换。

5. 为什么归并稳定,程序实现还不用归并?

快排的空间复杂度是Θ(lgn),归并的空间复杂度为O(n);

多线程

1. 协程是什么?

2. 协程和多线程,多进程的区别和联系;

多进程:操作系统中同时运行的多个程序

多线程:在同一个进程中同时运行的多个任务

举个例子,多线程下载软件,可以同时运行多个线程,但是通过程序运行的结果发现,每一次结果都不一致。 因为多线程存在一个特性:随机性。造成的原因:CPU在瞬间不断切换去处理各个线程而导致的,可以理解成多个线程在抢CPU资源。

多线程提高CPU使用率

多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。

3. 线程和进程的区别;

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

4. 线程和进程的通信方式;

多个进程之间相互通信,交换信息的方法。根据进程通信时信息量大小的不同,可以将进程通信划分为两大类型:
信号量(semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
2、高级通信:大批数据信息的通信(主要用于进程间数据块数据的交换和共享)

管道(pipe):管道是一种半双工的通信方式,同一时间数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。有名管道(namedpipe),有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

消息队列( messagequeue ) :消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存(shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

套接字(socket ):套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信。

线程间的通信方式
1、锁机制

包括互斥锁、条件变量、读写锁

a.互斥锁提供了以排他方式防止数据结构被并发修改的方法。

b.读写锁允许多个线程同时读共享数据,而对写操作是互斥的。

c.条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2、使用全局共享变量

主要由于多个线程可能更改全局变量,因此全局变量最好声明为Volatile

3、Object类的wait()、notify()和notifyAll()方法

4、使用消息队列实现通信
在Windows程序设计中,每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环)。

5、信号量机制(Semaphore)

Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

数据库

1. 说说数据库的索引

mysql索引用来加快对数据的访问,对于不同类型的索引,是和不同的存储引擎相关的。如果是MyISAM和InnoDB的存储引擎,是B+树,如果是Memory存储引擎的话,是哈希表。不同的存储引擎表示的是不同数据在磁盘的存储形式,k-v 格式的数据,不管任何类型的二叉树,都会让树变高,从而影响了 IO 的效率。B+树就是让树变低,从而提高访问速率。

2. 数据库索引结构有哪些?

(1)生成索引,建立二叉查找树 生成索引

(2)建立B-Tree 生成索引

(3)建立B±Tree 生成索引

(4)建立Hash,基于InnoDB和MyISAM的Mysql不显示支持哈希(5) 位图数据结构,BitMap,少量主流数据库使用,如Oracle,mysql不支持;

3. 如何定位并优化慢查询sql

(1)根据慢日志定位慢查询SQL

(2)使用explain等工具分析SQL

(3)修改SQL或者尽量让SQL走索引以优化查询效率

3. 索引是建立的越多越好吗?

(1)数据量小的表不需要建立索引,建立会增加额外的索引开销;
(2)数据变更需要维护索引,因此更多的索引意味着更多的维护成本;
(3)更多的索引也意味着需要更多的空间

4. 聚簇索引和非聚簇索引

聚集索引与非聚集索引的区别是:叶节点是否存放一整行记录。

InnoDB 主键使用的是聚簇索引,MyISAM 不管是主键索引,还是二级索引使用的都是非聚簇索引。

5. 列式存储与行式存储

6. B树、B+树、平衡二叉树区别

平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构;平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;

B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子。

B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别:

  1. B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
  2. B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
  3. B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针;

大数据

1. 谈一下HDFS

– 1.1 HDFS提出背景:
在多个节点协同工作时,两个以上的节点不能在同一时间读/写和操作同一文件。于是就想着将网络上的硬盘统一规划处理,来扩大存储空间,并解决同时读/写的问题。目的是想让不同的机器、不同的作业系统可以彼此分享文件,开始是为局域网的本地数据服务,后来发展到分布式文件系统,扩展到了整个网络。
– 1.2 分布式文件系统:
文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连,将固定在某个地点的某个文件系统扩展到多个地点/文件系统。在使用分布式文件系统时,人们就不用关心数据是存储在哪个节点上或者从哪个节点获取,只需像使用本地文件系统一样存储和管理文件系统的数据。
– 1.3 HDFS系统架构
HDFS是分布式计算中数据存储和管理的基础,主要由4个基本要素组成: 元数据节点NameNode,数据节点DataNode,从元数据节点,以及客户端。
– 1.4 四个基本要素的作用:
NameNode:作为分布式文件系统的管理者,用于管理文件系统的命名空间、存储块的备份及集群的配置信息等。元数据节点会将文件系统的元数据(metadata)存储在内存,这些内容记录文件的基本信息,即该文件有哪些数据库,这些数据库怎样分布在哪些数据节点上。一个集群中只有一个元数据节点。

数据节点datanode:以数据块为单位进行得数据存储,将所有存储得数据块信息周期性得发给元数据节点,客户端和元数据节点沟通后,可以向数据节点请求读出或写入操作。

数据块和普通文件系统得区别:HDFS中,如果一个文件小于一个数据块得大小,那么它并不占用整个数据块的存储空间,而是有多少占用多少。

从节点:
(从节点提出的场景)-- 在元数据中保存有命名空间镜像文件和修改日志。如当文件系统客户端进行写操作时,首先将它记录在修改日志中,每次的写操作成功之前,修改日志会同步到文件系统。但是当数据量和数据操作非常大时,就会出现日志文件过大的问题。
(从节点的作用)-- 为了防止日志文件过大,从节点周期性的将元数据节点的命名空间镜像文件和修改日志合并,帮助元数据节点将内存中的元数据信息存储到硬盘上。合并后的命名空间镜像文件在元数据节点上也保存了一份,在元数据节点失败的时候可以进行恢复。


HDFS1.0 中只包含一个名称节点会带来哪些问题 (单点故障问题)。
单点故障问题:虽然 HDFS1.0 中存在第二名称节点,但是在 1.0 版本中第二名称节点的作用是周期性的从名称节点获取命名空间镜像文件 (FsImage) 和修改日志(EditLog),从而来对 FsImage 的恢复。因此当名称节点发生故障时,系统无法实时切换到第二名称节点以对外提供服务,仍需要停机恢复。(可以通过 HA 来解决)

在可扩展性方面,名称节点把整个 HDFS 文件系统中的元数据信息都保存在自己的内存中,HDFS1.0 中只有一个名称节点,不可以水平扩展,而单个名称节点的内存空间是由上限的,这限制了系统中数据块、文件和目录的数目。在系统整体性能方面,整个 HDFS 文件系统的性能会受限于单个名称节点的吞吐量。在隔离性方面,单个名称节点难以提供不同程序之间的隔离性,一个程序可能会影响会影响其他运行的程序。(通过 HDFS 联邦来进行解决)


请描述 HDFS HA 架构组成组建及其具体功能。
设置两个名称节点,其中一个名称节点处于 “活跃” 状态,另一个处于 “待命” 状态。处于活跃状态的名称节点负责对外处理所有客户端的请求,而处于待命状态的名称节点则作为备用节点。处于待命状态的名称节点提供了 “热备份”,一旦活跃名称节点出现故障,就可以立即切换到待命名称节点,不会影响到系统的正常对外服务。


在 HDFS HA 中,所有名称节点会共享底层的数据节点存储资源。每个数据节点要向集群中所有的名称节点注册,并周期性地向名称节点发送 “心跳” 和块信息,报告自己的状态,同时也会处理来自名称节点的指令。


客户端:需要获取HDFS系统中文件的应用程序和接口,引起HDFS的读/写操作。

– 1.5 HDFS的数据流设计
文件读取:客户端向元数据节点发送文件读取请求,元数据节点返回文件存储的数据节点信息,客户端读取文件信息。
文件写入:客户端向元数据节点发起文件写入请求,元数据节点根据文件大小和文件块的配置情况,返回给客户端它所管理部分数据节点的信息。客户端将文件划分为多个数据块,根据数据节点的地址信息,按顺序写入到每一个数据块中。

– 1.6 HDFS优点
存储大数据。一次写入多次读写。容错性强。采用block块机制存储。

缺点:无法高效存储大量小文件;

2. 谈一下Hive

– Hive的产生场景:
Hive是一种数仓工具,可以用Hive将结构化的数据文件映射为一张数据库表,并提供完整的SQL查询,可以将SQL转化为MapReduce任务运行。(Hive本身不具备存储功能)

– Hive和数据库的区别:
数据存储位置:Hive建立在Hadoop之上,所有Hive数据都是存储在HDFS中的;数据库可以将数据保存在块设备或本地文件系统里。

– Hive体系构成:由两部分组成–客户端组件和服务器端组件。
客户端组件包括–CLI、Client、WebGUI。服务器端组件包括Driver组件、MetaStore组件。
MetaStore组件是Hive元数据的集中存放地,包括MetaStore服务和后台数据存储,后台数据存储的介质是关系数据库。

– Hive执行流程:
将HQL语句转化为一系列可以在Hadoop mapreduce集群中运行的job。通常在客户端执行hive命令,然后输入SQL语句,Hive将SQL语句生成多个MapReduce的job,然后将这些job提交给Hadoop执行,执行完成后,再把结果放入HDFS或者本地的临时文件中。

– Hive 对SQL语句的执行过程:一条SQL语句的最终目的是将一张表或者若干表中的所有行数据一条条的进行处理,最终生成一组目标记录。因此首先需要将处理过程分解为若干个算子,然后将初始的表数据记录依次通过这些算子进行计算,最终得出结果。

例如:select a from tb1 where b > 1 order by c,对于这条sql语句,首先是需要一个table scan算子从表中读出数据,然后读出的数据经过一个flter算子过滤那些不满足条件b>1的数据,最后经过一个fetch算子将正确的数据返回。

– Hive的数据模型:Database、Table、Partition、Bucket。
Database:相当于关系数据库中的命名空间,作用是将用户和数据库的应用隔离到不同的数据库或模式中。主要有:create databae dbname、use dbname、drop database dbname这些语句。

Table:有两种类型,一种叫内部表,这种表的数据文件存储在Hive的数据仓库中,另一种是外部表,这种表的数据文件可以存放在Hive数据仓库外部的分布式文件系统上,也可以放在Hive数据仓库里。(Hive的数据仓库是HDFS的一个目录,这个目录是Hive数据文件存储的默认路径,它可以在Hive的配置文件里进行配置,最终也会存放到元数据库里。)

Partition:根据分区列的值对表数据进行粗略划分的机制。在Hive存储上体现为:表的主目录下的一个子目录,这个文件夹的名字是分区列定义的名字。

分区列不是表的某个字段,而是独立的列,根据这个列来存储表中的数据文件,使用分区的目的是为了加快数据的查询速度。在查询某个具体分区列里的数据时,没有必要进行全表扫描。

Hive中表中的一个Partition对应于表下的一个目录,所有的Partition的数据都存储在对应的目录中。

Bucket:Bucket是针对数据源数据文件本身来拆分数据的,使用桶的表会将源数据文件按一定规律拆分为多个文件。共同点:table和Partition都是目录级别的拆分数据。

Bucket将表的列通过hash算法进一步分解为不同的文件存储,其对指定列计算hash,根据hash值来切分数据,目的是为了并行,每个bucket对应一个文件。如将user列分散至32个buckets,首先对user列的值来计算hash,对应hash值为不同的hdfs的目录不同。

3. 谈一下Hbase

Hbase是面向列的分布式存储系统。

传统关系型数据库是面向行的,数据库表中的每一行都是一条独立的数据,存储在磁盘上的一块区域内,通过标记哪一列是什么值来存储。

(列数据库产生背景)–
面向行数据库存储的缺点:浪费磁盘空间,如果一行数据需要插入到数据库中,数据库就会为这行数据根据数据类型来开辟存储空间,空间开出来时,尽管一些字段是空的,这个存储空间也会被占用。

查询消耗资源。虽然有时select只查询某些列,但是实际会将满足条件的所有数据反馈在客户端,客户端再进行过滤。

面向列的数据库存储:不同的列存储在不同的磁盘文件中,是按照不同的列簇来存储的。列簇是具有某个相同特征的列的集合。

(HBase设计思想)–

2. HDFS和Hbase存储的区别是什么?

3. 行式和列式存储的区别

① 行式存储使用 NSM 存储模型,一个元组 (或行) 会被连续的存储在磁盘页中。(存) 数据是一行行存的,当第一行写入磁盘页后,再继续写入第二行。(读取)** 从磁盘中读取数据时,需要从磁盘中顺序扫描每个元组的完整内容,然后从每个元组中筛选出查询所需要的属性;

② 列式存储使用 DSM 存储模型,DSM 会对关系进行垂直分解,并为每个属性分配一个子关系。因此,一个具有 n 个属性的关系会被分解成 n 个子关系,每个子关系单独存储,每个子关系只有当其相应的属性被请求时才会被访问。故列式存储以关系数据库的属性为单位进行存储,关系中多个元组的同一属性值会被存储再一起,而一个元组中不同属性值则通常会被分别存放于不同的磁盘页中;

③ (两者区别) 行式存储适合小批量的数据处理,但是适合于复杂的**

4. Hive的SQL转MapReduce的实现过程;

看书!!!!85页;

5. mp的shuffle过程

Shuffle 过程是 MapReduce 工作流程的核心,试分析 Shuffle 过程的作用。

将无序的 <key,value> 转成有序的 < key,value-list>,目的是为了让 Reduce 可以并行处理 Map 的结果。

分别描述 Map 端和 Reduce 端的 Shuffle 过程 (需包括 spill、Sort、Merge、Fetch 的过程)

Shuffle 就是对 Map 输出结果进行分区、排序、合并等处理一并交给 Reduce 的过程。Shuffle 过程分为 Map 端的操作和 Reduce 端的操作:

在 Map 端的 Shuffle 过程:Map 端的 Shuffle 过程分为四个步骤,① 输入数据和执行 Map 任务;② 写入缓存 (分区,排序和合并);③ 溢写;④ 文件归并;
① Map 任务的输入数据一般保存在分布式文件系统的文件块中,接受 <key,value> 作为输入,按一定映射规则转换为一批 < key,value > 作为输出;

② Map 的输出结果首先写入缓存,在缓存中积累一定数量的 Map 输出结果后,再一次性批量写入磁盘,这样可以大大减少对磁盘的 IO 消耗。因为寻址会开销很大,所以通过一次寻址、连续写入,就可以大大降低开销;(写入缓存之前,key 和 value 值都会被序列化成字节数组);

③ 因为缓存中 map 结果的数量不断增加,因此需要启动溢写操作,把缓存中的内容一次性写入磁盘,并清空缓存。通常采用的方法是到达 0.8 的阈值后,就启动溢写过程。在溢写到磁盘之前,缓存中的数据首先会被分区,默认分区方式是采用 Hash 函数对 key 进行哈希后再用 Reduce 任务的数量进行取模,表示为:hash(key) mod reduce任务数量,这样就可以把 map 输出结果均匀的分配给这 R 个 reduce 任务去并行处理了。分完区后,再根据 key 对它们进行内存排序,排序完后,可以通过用户自定义的 Combiner 函数来执行合并操作,从而减少需要溢写到磁盘的数据量。经过分区、排序以及可能发生的合并操作后,这些缓存中的键值对就可以被写入磁盘,并清空缓存。每次溢写操作都会在磁盘中生成一个新的溢写文件,写入溢写文件中的所有键值对都是经过分区和排序的。

④ 每次溢写操作都会在磁盘中生成一个新的溢写文件,随着 MapReduce 任务的进行,磁盘中的溢写文件数量越来越多,最后在 Map 任务全部结束之前,系统对所有溢写文件中的数据进行归并,从而生成一个大的溢写文件;

⑤ 经过上述四个步骤后,Shffle 过程完成,最终生成的一个大文件会被存放在本地磁盘上,这个大文件中的数据是被分区的,不同的分区会被发送到不同的 Reduce 任务进行并行处理。同时 JobTracker 会一直检测 Map 任务的执行,当检测到一个 Map 任务完成后,就会立即通知相关的 Reduce 任务来 “领取” 数据,然后开始 Reduce 端的 Shuffle 过程。

在 Reduce 端的 Shuffle 过程:Reduce 端的 Shuffle 只需要从 Map 端读取 Map 结果,然后执行归并操作,最后输送给 Reduce 任务进行处理。过程分为三个步骤,① 领取数据;② 归并数据;③ 把数据输入给 Reduce 任务;
① 领取数据
Map 端的 Shuffle 过程结束后,所有 map 输出结果都保存在 Map 机器的本地磁盘上,Reduce 需要把磁盘上的数据 fetch 回来存放在自己所在机器的本地磁盘上。故每个 Reduce 任务会不断的向 JobTracker 询问 Map 任务是否完成,当检测到一个完成后,就会通知相关的 Reduece 任务来领取数据,一旦一个 Reduce 任务收到了通知,就会到该 Map 任务所在机器上把属于自己处理的分区数据 fetch 到本地磁盘;

② 归并数据
因为缓存中的数据是来自不同的 Map 机器,一般会存在很多可以合并的键值对。当溢写过程启动时,具有相同 key 的键值会被归并,归并时会对键值对进行排序,以保证最终大文件的键值对都是有序的。最终通过多轮归并将磁盘上多个溢写文件归并为多个大文件。

③ 把数据输入给 Reduce
磁盘经过多轮归并后得到的若干个大文件,会直接输入个 Reduce 任务 (不会归并成一个新的大文件)。

6. Spark的shuffle过程;

产生背景:Spark Shuffle 示例都是以 MapReduce Shuffle 为参考的,所以下面提到的 Map Task 指的就是 Shuffle Write 阶段,Reduce Task 指的就是 Shuffle Read 阶段。

因此不在 Shuffle Read 时做 Merge Sort,如果需要合并的操作的话,则会使用聚合(agggregator),即用了一个 HashMap (实际上是一个 AppendOnlyMap)来将数据进行合并。

在 Map Task 过程按照 Hash 的方式重组 Partition 的数据,不进行排序。每个 Map Task 为每个 Reduce Task 生成一个文件,通常会产生大量的文件(即对应为 M*R 个中间文件,其中 M 表示 Map Task 个数,R 表示 Reduce Task 个数),伴随大量的随机磁盘 I/O 操作与大量的内存开销。

引入了 File Consolidation 机制

一个 Executor 上所有的 Map Task 生成的分区文件只有一份,即将所有的 Map Task 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件。

这样就减少了文件数,但是假如下游 Stage 的分区数 N 很大,还是会在每个 Executor 上生成 N 个文件,同样,如果一个 Executor 上有 K 个 Core,还是会开 K*N 个 Writer Handler,所以这里仍然容易导致 OOM。

每个 Task 不会为后续的每个 Task 创建单独的文件,而是将所有对结果写入同一个文件。该文件中的记录首先是按照 Partition Id 排序,每个 Partition 内部再按照 Key 进行排序,Map Task 运行期间会顺序写每个 Partition 的数据,同时生成一个索引文件记录每个 Partition 的大小和偏移量。

在 Reduce 阶段,Reduce Task 拉取数据做 Combine 时不再是采用 HashMap,而是采用 ExternalAppendOnlyMap,该数据结构在做 Combine 时,如果内存不足,会刷写磁盘,很大程度的保证了鲁棒性,避免大数据情况下的 OOM。

总体上看来 Sort Shuffle 解决了 Hash Shuffle 的所有弊端,但是因为需要其 Shuffle 过程需要对记录进行排序,所以在性能上有所损失。

7. 数仓分层

简单点儿,直接ODS+DM就可以了,将所有数据同步过来,然后直接开发些应用层的报表,这是最简单的了;当DM层的内容多了以后,想要重用,就会再拆分一个公共层出来,变成三层架构;

DWD明细数据层, DWS汇总数据层,ADS应用数据层,
在这里插入图片描述

笔试题

1. 两个字符串,按照规则判断相等(重写equals),规则是两个字符串相同字符出现的次数相同,遍判定相等。例(AAB 和 ABA 相等)

方法一:排序法
对两个字符串中的字符进行排序,比较两个排序后的字符串是否相等。若相等,则表明他们是由相同的字符组成的,否则,表明它们不是由相同的字符组成的。
方法二:用集合进行字符次数的统计;

2. 怎么样判断一个链表是否成环

先让两个指针从起点开始走,一个每次走一步,另外一个每次走两步。相遇之后再让其中一个指针走向起点,每次两个人走一步,当再次相遇时,就走到了环的入口。

3. 求Pi

根据公式:pi/4 = 1-1/3+1/5-1/7;

4. 大数相加

数据结构

  1. 讲一下平衡树的特征。
    任意节点的子树的高度差都小于等于1,当执行插入或删除操作时,只要不满足条件,就会通过旋转来保持平衡,AVL树适用于插入删除次数比较少,但查找多的情况。

  2. 介绍下HashMap;
    HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
    当链表过长时会将链表转成红黑树以实现O(logn)时间复杂度的查找;
    每一个entry都含有以下四个元素:key . value . next .hash ,不能理解为只是存储了key的值,但是我们存储或者取出元素的时候都是利用key值计算hashcode值;

  3. HashMap的工作原理
    HashMap基于hashing原理,通过put()和get()方法储存和获取对象。
    put()方法:当将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,返回的hashCode用于找到bucket位置来储存Entry对象。如果该位置已经有元素了,调用equals方法判断是否相等,相等的话就进行替换值,不相等的话,放在链表里面;
    get()方法:当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象

  4. HashMap如果两个键的hashcode相同,你如何获取值对象
    当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。(只有两个对象相同.equals才会返回true)。

  5. HashMap是非线程安全的,如何使其变为线程安全的?
    调用工具类Collections.synchronizedMap(map):

     HashMap map =new HashMap();
     map.put("测试","使map变成有序Map");
     Map map1 = Collections.synchronizedMap(map);
    
  6. ArrayList和Vector的区别?
    vector在add的时候使用了同步函数,方法上加上了synchronized关键字ArrayList的add方法是没有加上这个关键字的。当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍。

  7. ArrayList与LinkedList的区别
    ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构;
    对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针;
    对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据;

  8. 简要介绍下红黑树;
    红黑树是一种弱平衡二叉树,其为每个节点增加了一个存储位来表示节点颜色,可以是red or black。相对于AVL树而言,其旋转次数变少,对于搜索,插入和删除操作多的情况,常用红黑树。

  9. map了解吗,说说hashmap,hashtable,treemap
    HashMap:HashMap主要用于存放键值对,由数组、链表、红黑树构成,他会对键的值进行Hash运算获得对应的索引去数组中存取节点,如果发生哈希冲突则会形成链表,新的元素将会放在最后,当链表长度大于8,数组容量大于64时会根据当前链表形成红黑树。当红黑树中节点小于6时会转换回链表;
    Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突
    ⽽存在的。
    TreeMap : 红⿊树(⾃平衡的排序⼆叉树);
    相关题目
    1.1 HashMap 和Hashtable 的区别

    1. 底层数据结构不同Hashtable 继承自Dictionary 类HashMap 是实现了Map 接口
    2. 线程是否安全Hashtable 是线程安全的,而HashMap 是线程不安全的,在多个线程访问Hashtable 时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步;
    3. 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null作为键只能有一个,null作为值可以有多个;HashTable不允许有null键和null值,否则会抛出NullPointException异常;
    4. 始容量⼤⼩和每次扩充容量大小不同

请添加图片描述

作者计算机硕士,从事大数据方向,公众号致力于技术专栏,主要内容包括:算法,大数据,个人思考总结等

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

精神抖擞王大鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值