目录
equals()和==区别。为什么重写equal要重写hashcode?
synchronized 和 ReentrantLock 的区别
tryLock和Lock和lockInterruptibly 的区别
大家好,我是月夜枫,本来是想一篇写完,后来发现一篇有点长就拆成了两篇文章,今天继续分享面试中可能会被问到的java基础,希望能给小伙伴们带来帮助,废话不多说,直接上干货!!!
技术面试中的几个注意点:
1 面试时,你熟悉的问题要和面试官多聊,不要为了回答问题而回答问题
2 一个问题的沟通时间最好能多聊一会儿,简单问题说3/5分钟,如果问题的规模比较大,10分钟左右也是可以的
3 回答问题时不要为了凑时间而凑时间,聊的内容一定要和问的问题相关,知识点可以连续的引入
4 了解的东西多聊,不了解的少说
5 对于知识可以有一些自己的见解,自己的想法,清晰表述出来,虽然自己的看法有时候不会特别的恰当.
== 是运算符 equals来自于Object类定义的一个方法
== 可以用于基本数据类型和引用类型
equals只能用于引用类型
== 两端如果是基本数据类型,就是判断值是否相同
equals在重写之后,判断两个对象的属性值是否相同
equals如果不重写,其实就是 ==
重写equals可以让我们自己定义判断两个对象是否相同的条件
Object中定义的hashcode方法生成的哈希码能保证同一个类的对象的哈希码一定是不同的
当equals 返回为true,我们在逻辑上可以认为是同一个对象,但是查看哈希码,发现哈希码不同,和equals方法的返回结果违背
Object中定义的hashcode方法生成的哈希码跟对象的本身属性值是无关的
重写hashcode之后,我们可以自定义哈希码的生成规则,可以通过对象的属性值计算出哈希码
HashMap中,借助equals和hashcode方法来完成数据的存储
将根据对象的内容查询转换为根据索引查询
数据结构
在Java1.7中,HashMap的数据结构为数组+单向链表。Java1.8中变成了数组+单向链表+红黑树
链表插入节点的方式
在Java1.7中,插入链表节点使用头插法。Java1.8中变成了尾插法。
hash函数
Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率
扩容优化:
扩容以后,1.7对元素进行rehash算法,计算原来每个元素在扩容之后的哈希表中的位置,1.8借助2倍扩容机制,元素不需要进行重新计算位置
JDK 1.8 在扩容时并没有像 JDK 1.7 那样,重新计算每个元素的哈希值,而是通过高位运算(e.hash & oldCap)来确定元素是否需要移动,比如 key1 的信息如下:
使用 e.hash & oldCap 得到的结果,高一位为 0,当结果为 0 时表示元素在扩容时位置不会发生任何变化,而 key 2 信息如下
高一位为 1,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数组长度hashmap,不必像1.7一样全部重新计算位置
HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.
方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.
方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.
下面对这2中实现方法从各个角度进行分析和比较.
方法一特点:
通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处,1)使用了经典的synchronized来进行互斥, 2)使用了代理模式new了一个新的类,这个类同样实现了Map接口.在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒. 优点:代码实现十分简单,一看就懂.缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差.
方法二特点:
重新写了HashMap,比较大的改变有如下几点.使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync. 这个特性调用CAS指令来确保原子性与互斥性.当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行.
优点:需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好. 缺点:代码繁琐
查看源代码
在存入元素时,放入元素位置有一个 (n-1)&hash 的一个算法,和hash&(newCap-1),这里用到了一个&位运算符
当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下
下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进制是01001,向里面添加同样的元素,结果为
可以看出,有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了
1开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
2再哈希法:
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
3链地址法
链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来
4建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
这里简单解释类加载器双亲委派:
无法实现隔离性:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的。部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM, web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
无法实现热替换:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。
打破双亲委派机制(参照JVM中的内容)OSGI是基于Java语言的动态模块化规范,类加载器之间是网状结构,更加灵活,但是也更复杂,JNDI服务,使用线程上线文类加载器,父类加载器去使用子类加载器
2. tomcat自己定义的类加载器:
CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和各个webapp访问
CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的class,即对webapp不可见
SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见
WebappClassLoader:webapp私有的类加载器,只对当前webapp可见
3. 每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个JspClassLoader,所以这两个类加载器有多个实例
4. 工作原理:
a. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用
b. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离
c. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系
d. 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能
5. tomcat目录结构,与上面的类加载器对应
/common/*
/server/*
/shared/*
/WEB-INF/*
6. 默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader的实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成了一个lib目录。所以在我们的服务器里看不到common、shared、server目录。
Java虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常
Java堆
对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。反射这种运行时动态的功能可以说是非常重要的,可以说无反射不框架!!!,反射方式实例化对象和,属性赋值和调用方法肯定比直接的慢,但是程序运行的快慢原因有很多,不能主要归于反射,如果你只是偶尔调用一下反射,反射的影响可以忽略不计,如果你需要大量调用反射,会产生一些影响,适当考虑减少使用或者使用缓存,你的编程的思想才是限制你程序性能的最主要的因素
AVL树
一般用平衡因子判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,AVL树是高度平衡的二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而的由于旋转比较耗时,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树
红黑树:
也是一种平衡二叉树,但每个节点有一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树红黑树从根到叶子的最长路径不会超过最短路径的2倍(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度<=红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,用红黑树
两者的共同点:
两者的不同点:
一个教室里面有很多同学,每个同学都要有自己的一个水杯.教室里还有一个饮水机,一个饮水机可以为教室内所有的同学提供用水,没有必要每个同学都准备一个饮水机.程序中往往一个类只需要一个对象就可以为整个系统服务,如果产生多个对象,消耗更多的资源.单例模式就是为了实现如何控制一个类只能产生一个对象. 单例模式控制控制对象不要反复创建,提高我们工作的效率.减少资源的占用
单例模式下类的组成部分
1私有的构造方法
2私有的当前类对象作为静态属性
3公有的向外界提供当前类对象的静态方法
但凡是控制一个类只能产生一个对象的模式都叫做单例模式,常见的有饿汉式,懒汉式,内部类式(接口/抽象类),静态内部类式 ... ...
- /*
- 多例
- 只要调用了构造方法 就会在内存上产生一个独立的空间
- 1将构造方法私有化
- 构造方法私有化了,外界不能new对象了?对象怎么产生?
- 2组合当前类本身作为私有静态属性并调用构造方法实例化
- 如何让外界获取属性值呢?
- 3在当前类中准备一个共有的静态方法向外界提供当前类对象
- */
- public class SingleTon {
- private static SingleTon singleTon =new SingleTon();
- private SingleTon(){
- }
- public static SingleTon getSingleTon(){
- return singleTon;
- }
- }
- class Test{
- public static void main(String[] args) {
- SingleTon st =SingleTon.getSingleTon();
- SingleTon st2=SingleTon.getSingleTon();
- System.out.println(st==st2);
- System.out.println(st);
- System.out.println(st2);
- }
- }
好处: 饿汉式单例模式在类加载进入内存初始化static变量是会初始化当前类对象,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费,不能延迟加载!
- /*
- 多例
- 只要调用了构造方法 就会在内存上产生一个独立的空间
- 1将构造方法私有化
- 构造方法私有化了,外界不能new对象了?对象怎么产生?
- 2组合当前类本身作为私有静态属性并调用构造方法实例化
- 如何让外界获取属性值呢?
- 3在当前类中准备一个共有的静态方法向外界提供当前类对象
- */
- public class SingleTon {
- private static SingleTon singleTon;
- private SingleTon(){
- }
- public static SingleTon getSingleTon(){
- if(null == singleTon){
- singleTon=new SingleTon();
- }
- return singleTon;
- }
- }
- class Test{
- public static void main(String[] args) {
- SingleTon st =SingleTon.getSingleTon();
- SingleTon st2=SingleTon.getSingleTon();
- System.out.println(st==st2);
- System.out.println(st);
- System.out.println(st2);
- }
- }
延迟加载,也叫作懒加载,等到真正用的时候才加载.
懒汉式代理模式在多线程并发情况下仍然是有可能创建多次,是线程非安全的
- public class Test1 {
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- SingleTon.getSingleTon();
- }
- }).start();
- }
- }
- }
- class SingleTon {
- private static SingleTon singleTon;
- private SingleTon(){
- System.out.println(Thread.currentThread().getName()+"创建了对象");
- }
- public static SingleTon getSingleTon(){
- if(null == singleTon){
- singleTon=new SingleTon();
- }
- return singleTon;
- }
- }
为了解决线程并发问题我们需要对其进行优化,作为一个双重检测式的单例模式,就是我们说的DCL单例模式
- package com.msb.singleTon;
-
- public class Test1 {
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- SingleTon.getSingleTon();
- }
- }).start();
- }
- }
- }
- class SingleTon {
- private volatile static SingleTon singleTon;
- private SingleTon(){
- System.out.println(Thread.currentThread().getName()+"创建了对象");
- }
- public static SingleTon getSingleTon(){
- if(null ==singleTon){
- synchronized (SingleTon.class){
- if(null == singleTon){
- singleTon=new SingleTon();
- /**
- * 1分配空间
- * 2执行构造方法
- * 3将创建对象的引用地址赋值值singleTon变量
- * 为了避免多线程下的指令重拍问题和多线程缓存造成的数据更新不及时问题
- * 我们应该在加上volatile处理
- */
- }
- }
- }
- return singleTon;
- }
- }
除此之外,我们还可以使用内部类实现单例模式的控制
- class Single{
- /*
- * 私有构造方法
- * */
- private Single(){
-
- }
- /*
- * 范围内部类的属性
- * */
- public static Single getSingle(){
- return InnerClass.single;
- }
- /*
- * 静态内部类
- * */
- public static class InnerClass{
- /*
- * 组合外部类对象作为属性
- * */
- private static final Single single=new Single();
- }
- }
外部类没有static属性,则不会像饿汉式那样立即加载对象,只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。 instance是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.兼备了并发高效调用和延迟加载的优势
- public class Test3 {
- public static void main(String[] args) {
- SingleTon1 s1=SingleTon1.INSTANCE;
- SingleTon1 s2=SingleTon1.INSTANCE;
- s1.singleTonOperation();
- System.out.println(s1==s2);
- }
- }
-
- enum SingleTon1{
- INSTANCE;
- public void singleTonOperation(){
- System.out.println("operation");
- }
- }
优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
缺点:无延迟加载
单例模式总结:
单例模式主要的两种实现方式
饿汉式 线程安全,调用效率高,不能延时加载
懒汉式 线程安全,调用效率不高,可以延时加载
其他方式
双重检测锁式 极端情况下偶尔会出现问题,不建议使用
静态内部类式 线程安全,调用效率高,可以延时加载
枚举式 线程安全,调用效率高,不能延时加载