基础篇
Java基础
1. 面象对象的特征:继承、封装、多态;
- 封装:是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对信息的操作和访问;
- 封装的好处:
- 隐藏类的实现细节;
- 限制对成员变量的不合理访问;
- 可以进行数据检查,有利保证对象的完整性
- 封装的好处:
- 继承:多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,秩序继承那个类即可;
- 优点:
- 提高了代码的复用性、维护性;
- 让类与类之间产生了关系,这是多态的前提;
- 缺点:类之间的耦合性增强了;
- 特点:java只支持单继承,不支持多继承;Java可实现多层继承;
- 注意:
- 子类只能继承父类所有非私有的成员;
- 不能继承构造方法,但可以通过super关键字访问;
- 不要为了部分功能而去继承;
- 优点:
- 多态:同一个方法可以根据调用对象的不同而产生不同的行为;
- 三个必要条件:
- 有继承;
- 有重写;
- 父类引用指向子类对象;
- 编译看左边,运行看右边
- 三个必要条件:
2. final、finally、finalize的区别
-
final:可以修饰类、方法、变量;修饰的类不能被继承,修饰的方法不能被重写,被修饰的变量只能赋值一次,相当于常量;
-
finally:是异常处理中的一部分,表示这段语句最终一定会被执行而不管是否发生异常,经常用于需要释放资源的情况下;
有三种情况finally中的代码不会被执行:
(1)在try块之前出现异常
(2)在执行时java虚拟机被终止;
(3)在执行try或catch时被打断或被终止;
特殊情况:如果在finally中使用return,则会撤销之前的return语句,继续执行最后的finally中的代码; -
finalize:在Object理被定义,也就是每一个对象都有这个方法,该对象被回收的时候调用;
3. Exception、Error、运行时异常与一般异常有何异同
- Error:一般是指与虚拟机相关的问题,这种错误无法恢复或不可捕获,将导致应用程序终端;
- Exception:程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理;
- Exception又可分为检查异常(编译异常)和不检查异常(运行时异常)
Checked:必须进行捕捉处理; - RunTime:可以不处理,总是由虚拟机接管;一层层往上抛,直到遇到处理代码,如果没有遇到处理代码,则由虚拟机处理,终止程序,打印异常;
4. 请写出5种常见到的runtime exception
- IndexoutOfBoundsException 数组越界异常
- NullPointerException 空指针引用异常
- NumberFormatException 数字格式异常
- ClassCastException 类型强制转换异常
- IllegalArgumentException 传递非法参数异常
5. int 和 Integer 有什么区别,Integer的值缓存范围
- Integer是int的包装类;int是基本数据类型;
- Integer变量必须实例化后才能使用;int 变量不需要;
- Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值;
- Integer默认值是null;int的默认值是0;
- 缓存值范围为-128-127
6. 包装类,装箱和拆箱
- 包装类:Java为每一个基本数据类型都引入了对应的包装类型,为了能够将这些基本数据类型当成对象操作;因为java中很多地方用到的是对象而不是基本数据类型,比如:集合类中无法将int等类型放进去;
- 装箱:将基本数据类型重新转化为对象(对应的包装类);
- 拆箱:将对象(对应的包装类)重新转换为基本数据类型;
- 自动装箱都是通过包装类的valueOf方法实现的;自动拆箱都是通过xxxValue实现的;
- 自动拆装箱的设计模式是享元模式;
- 带来的问题:
- 包装对象的数值比较不能简单地使用==,在缓存范围外要使用equals;
- 如果包装类为null,拆箱时会抛出异常;
- 如果一个for循环中有大量拆装箱,会浪费很多资源;
7. String、StringBuilder、StringBuffer
- String是字符串常量使不可变的对象,每次改变时都相当于创建了新的对象然后将引用指向新对象,如果经常改变string的值最好不要用,因为会产生许多无引用的垃圾对象,JVM的GC就会回收,影响系统的性能;
String S1 = “This is only a” + “ simple” + “test”对于这种字符串拼接不需要太多时间,而如果拼接的字符串来自不同的string对象,则时间较慢; - StringBuffer是线程安全的可变字符序列,类似于String的字符串缓冲区;
- StringBuilder是线程不安全的可变字符序列,适合于在单线程使用;
StringBuiler>StringBuffer>String
8. 重载和重写的区别
- 重载:是让类一统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者类型的同名函数,同时存在于同一个类,是一个类中多态性的一种表现;
- 规则:
- 必须具有不同的参数列表(参数类型、参数个数);
- 可以有不同的返回类型;
- 可以有不同的访问修饰符;
- 可以抛出不同的异常;
- 重写:是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写;
- 规则:
- 参数列表必须完全与被重写的方法相同;
- 返回类型必须相同;
- 访问修饰符的限制一定要大于等于被重写方法的访问修饰符;
- 一定不能抛出比被重写方法申明更加宽泛的检查型异常;
重载和重写是Java多态性的不同表现;
重写是父类与子类之间多态性的表现,在运行时起作用;
重载是同一个类中多态性的表现,在编译时起作用;
9. 抽象类与接口的区别
-
抽象类: 用abstract修饰的类叫做抽象类。
-
特点:
- 抽象类不能有对象(不能用new 此关键字来创建抽象类的对象);
- 有抽象方法的类一定是抽象类,但是抽象类中不一定有抽象方法;
- 抽象类中的抽象方法必须在子类中重写;
-
注意:
- abstract不能和private共存;
- abstract 不能和final 共存;
- 和static 没有意义,通过类来调用一个没有方法体的方法会报错;
-
接口:意义是为系统提供更好的扩展性和可维护性;
-
特点:
- 接口里不能包含构造器和初始化代码块;
- 接口里的成员变量只能是静态变量,并且还是被final修饰的;
- 1.8之后接口里的方法可以是抽象方法、类方法和默认方法(被default修饰);
- 接口的子类可以使抽象类;
- 接口和接口之间可以是多继承;
- 接口中的成员变量默认为 public static final;方法默认为public static;
接口与抽象方法:
- 相同点:
- 都不能被实例化,只能用于被其他类实现和继承;
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法;
- 不同点:
- 1.8之后接口里能包含抽象方法、静态方法和默认方法,不能定义普通方法,其中静态方法要有方法体,而且不需要被实现,方法默认被public abstract修饰;而抽象类中可以包含普通方法;
- 接口里不包含构造器,抽象类可以包含;
- 接口里不能包含初始代码块,抽象类可以;
- 抽象类不能实现多继承,接口可以;
- 接口中只能定义静态常量,不能定义普通成员变量;而抽象类可以;
10. 说说反射的用途及实现
- 对反射的理解:一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,于是可以直接对这个类进行实例化,这样可以理解为"正";而反射则是一开始并不知道我们要初始化的类对象是什么,自然就无法使用new 关键字来创建对象了;所以是在运行时才加载、探知在编译期不知道的这个类。
- 反射的概念:在运行时构造一个类的对象;判断一个类所具有的成员变量和方法;调用一个对象的方法;生成动态代理;
- 功能:
- 获取对象的类信息;
- 获取一个类的访问修饰符、成员、方法、构造方法以及超类信息;
- 检获属于一个接口的常量和方法声明;
- 创建一个直到程序运行期间才知道名字的类的实例;
- 设置并获取一个直到程序运行期间才知道名字的类的成员;
- 检测一个直到程序运行期间才知道名字的对象方法;
- 应用:
- 例如idea、eclipse的方法属性自动提示功能,就是利用反射;
- spring的ioc/di,aop的动态代理;
- javaBean和jsp之间调用;
- JDBC的classForName;
11. 说说自定义注解的场景及实现
- 实现:
@Documented 注解是否包含在javaDoc中;
@Retetion 什么时候使用该注解
@Target 注解用于什么地方
@Inherited 是否允许子类继承该注解
- 原理:注解本质是一个继承了Annotation的特殊接口,其具体实现类时java运行时生成的动态代理。而我们通过反射获取注解的类时,返回的是java运行时生成的动态代理对象,通过代理对象调用自定义注解的方法。
- 常用场景:
- 登陆、权限拦截;
- 定时任务管理;
Http长连接短连接?
Http/1.0 及之前是短连接
Http/1.1 及之后是长连接:服务器返回的响应中携带要返回内容的长度,浏览器接受的内容如果小于这个长度就不显示,直到接收到所有的内容,才显示出来。
因为以前的网页内容请求不多,使用短连接;现在随着技术的发展,一个网页中有较多的请求,所以使用长连接比较节省资源。
12. HTTP请求的GET与POST方式的区别
https://www.cnblogs.com/logsharing/p/8448446.html
- get产生一个tcp数据包,post产生两个tcp数据包;
- get是从服务器上获取数据,post是向服务器传送数据;
- get是把参数数据队列加到提交表单Action属性所指的url中,在url中可看到;
post是通过HTTPpost机制,将数据放置在HTML HEADER 内一起传送到Action属性所指的url地址中,用户看不到这个过程;但是可以通过开发者工具或抓包看到。 - get 传送的数据量有限制,post传动的数据量无限制;get直接搜ASCII字符,而post无限制;
- get请求会被浏览器主动缓存,而post不会,除非手动设置;get请求参数会被完整保留在浏览器历史记录,而post不会;
- get 安全性低,post 安全性高;
13. Session与Cookie区别
-
cookie 为了解决HTTP协议无状态的缺陷所做的努力,是作为HTTP传输头信息的一部分发给客户机的
- cookie的分发:服务器通过在HTTP的响应头中加一行特殊的指示以提示浏览器按照指示生成相应的cookie。
- cookie的使用:由浏览器按照一定的原则在后台发送给服务器;
-
区别
- cookie数据存放在客户端的浏览器上,session数据放在服务器上;
- cookie只能存储String类型,而session可以存储任意类型;
- coolie不安全,别人可以分析放在本地的coolie并进行cookie欺骗
- 单个cookie的数据不超过4K,很多浏览器都限制最多保存20个cookie;
- session会在一定时间内保存在服务器上,会比较占用服务器的性能,从这方面考虑,应使用cookie;
-
生命周期:
- 会话cookie的失效是浏览器的关闭,存在内存中,持久cookie是存在磁盘中;
- session都有一个有效期,一般容器都会有一个后台线程用于检查session是否失效,而且调用
request.getsession
时会检查对应session,如果失效则重新创建。在应用容器重启过关闭的时候,未过期的session对象会持久化到一个文件中,当容器再次启动时,会重新读取文件所有未过期的session对象。
14. 列出自己常用的JDK包
- java.lang:系统基础类
- java.sql:数据库操作类
- java.io:所有输入输出有关的类
- java.math:数学工具类
- java.util:系统辅助类
15. MVC设计思想
-
M Model 模型:业务逻辑包含了业务数据的加工处理以及相应的基础服务;
-
V View 视图 :展现模型处理的结果,另外还要提供相应的操作界面;
-
C Controller 控制器:视图发请求给控制器,有控制器来选择相应的模型来处理;模型返回结果给控制器,有控制器选择合适的视图展示;
-
MVC的优点:
- 低耦合性:前后端分离,更有效率;
- 高可用性和可适用性;
- 较低的生命周期成本;
- 快速部署;
- 可维护性;
-
缺点:
- 增加了代码量、设计难度增加;
16. equals与==的区别
- ==
- 对于基本数据类型,==比较的是它们的值;
- 对于引用数据类型,==比较的是它们的地址;
- equals:默认情况下,比较的是地址值,不过可以根据情况重写该方法,如String、Date等类对equals方法进行了重写的话,比较的是所指对象的内容;不能用于基本数据类型变量;
17. hashCode和equals方法的区别与联系
- 当覆盖了equals方法时,比较对象是否相等通过覆盖后的equals方法进行比较;
- hashcode是为了提高在散列结构存储中查找的效率,在线性表中没有用;
- hashCode方法只要set或map集合中用到;
- 将对象放到集合中时,首先通过equals判断要放入对象的hashcode值与集合中任意一个元素的hashcode是否相等,如果不相等在将对象放入,如果想等则不放入;
- 若equals判断相等,则hashcode有必要返回相同的int 数;
- 若equals判断不相等,则hashcode不一定返回不同的int 数;
- 若两对象hashcode返回相同int 数,equals不一定判断相等;
- 若两对象hashcode返回不同的int 数,equels 一定判断不相等;
- 由于equals比较效率较低,所以当需要对比的时候,先用hashcode去对比,如果hashcode不相等则对象肯定不相等,否则。再使用equals去比较,如果也相等则对象相等。
重写之后如何计算hashcode?
可以看到重写后的hashcode将对象的属性放入一个Object[]中。然后以计算字符串hash值的方式计算属性值的hash值。
字符串计算hash值,s[n]代表的是第n个字母ASCII码。
s.hashcode=s[0]*31^{n-1} + s[1]*31^{n-2}+s[2]*31^{n-3} ... s[n-1]*31^{n-n}
@Override
public int hashCode() {
return Objects.hash(name, sex, age);
}
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
未重写之前如何计算hashcode?
根据地址对象信息等映射出来的一个整数值,是由本地方法实现的,也就是非Java的方法来完成的。
重写后的equals如何比较?
先判断两个对象是否是同一个对象,如果是直接返回true,
如果对象为null或者不是同一个类的对象直接返回false;
否则逐一比较两个对象的属性值是否相等。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name) &&
Objects.equals(sex, user.sex) &&
Objects.equals(parent, user.parent);
}
为什么重写equsls之后必须重写hashcode?
因为在未重写之前,对于引用对象equals比较的是两对象的地址值;hashcode比较的是根据地址值映射出来的hash值。
而重写之后,equals比较的对象属性内容,如果不重写hashcode,会导致equals比较出来为true而hashcode值却不相等。比如将对象放入hashmap、hashset等集合时,会出现问题。
18. 什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用
- 序列化概念:序列化就是一种用来处理对象流的机制,对象流就是将对象的内容进行流化,将对象转换为二进制,序列化是为了解决在对象流进行读写时所引发的问题。把对象转换为字节序列的过程称为对象的序列化,把字节序列恢复为对象的过程称为对象的反序列化;
- 将需要被序列化的类实现Serializable接口,这个接口只是用来标记该类需要被序列化。
- 相关的类和接口:在java.io包中提供的涉及对象的串行化的类与接口有ObjectOutput接口、ObjectOutputStream类、ObjectInput接口、ObjectInputStream类
- 如果类中属性引用了别的类,那么被引用的类也会被序列化,称为递归序列化;
- 如果某个类能被序列化,其子类也可以被序列化;如果该类有父类,则分为两种情况,若其父类已经被序列化则其父类的相应字段及属性的处理和该类相同,否则,父类所有的字段属性将不会序列化;
- 如果不想序列化类的所有属性,则给该属性加上
transient
关键字修饰,则序列化时会忽略这个属性;还有声明为static的成员不能被序列化,因为代表类的状态; - 序列化的用途:
- 把对象的字节序列永久地保存在硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列;
- tomcat在重启时把未过期的session对象序列化到硬盘;等再次启动时又反序列化到自身容器;
19. Object类中常见的方法,为什么wait notify会放在Object里边?
- 常见的方法:
- toString 输出一个对象的地址字符串,重写后可以获取对象的属性;
- clone:创建并返回此对象的一个副本;
- finalize:当垃圾回收器确定不存在对该对象的更多饮用时,有对象的垃圾回收器调用此方法;
- hashcode:返回对象的hash码值;
因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说等待和唤醒必须是同一个锁,而所可以使任意对象,所以可以被任意对象调用的方法是定义在object类中。
wait方法能不能被重写,wait能不能被中断;
wait方法底层原理
20. Java的平台无关性如何体现出来的
- java是凭借Java虚拟机来实现平台无关性的;
- java虚拟机面向编译器给其提供相同的接口,而虚拟机的解释器针对不同平台而不同;
- 为了适应多样性的运行环境,Java编译器生成字节码,一种被设计用来将代码高效地转换到多种硬件和软件平台的架构无关的中间格式;
- 支持平台无关性的原因:
- 支持多变的网络环境;
- 支持网络化嵌入式设备;
- 减少开发者部署程序的成本和时间;
21. JDK和JRE的区别
- JDK :是指java开发工具集,JDK是整个java的核心,包括了java基础类库、Java运行环境(JRE) 和开发工具;
- JRE:是指Java运行时环境,Java程序运行必须JRE的支持,如果系统只安装JRE的话,意味着系统可以跑任何Java程序,但不能做Java开发;
- 个人理解:JDK是给开发者用的,因为JDK包含基础类库,其中也包含JRE,因为开发者要不断调试程序;JRE是给客户用的,开发者编写的程序需要运行,所以客户端必须由JRE才能运行;也就是说JDK包含了JRE,但是JRE可以独立安装;
图片转自:https://blog.csdn.net/qq_39975542/article/details/81415225
22. Java 8有哪些新特性
- Lambda表达式和函数式接口;
- 接口的默认方法和静态方法;
- 方法引用:其实也是一种Lambda表达式;当这个已存在的方法仅仅是被调用时,可以使用::来调用
- 构造方法引用:
Class::new
- 静态方法引用:
Class::方法名
普通方法类似
- 构造方法引用:
- 重复注解:允许在同一个地方多次使用同一个注解;
- 拓宽了注解的应用场景:基于可以使用在所有元素上,可以使用在局部变量、接口类型、超类和接口实现类;
- Optional:引入了Optional来防止空指针异常;
- Stream:支持一连串连续、并行聚集操作的元素;
- Date/Time API
- JavaScript 引擎Nashorn:允许在JVM上开发运行JavaScript,允许Java和JS相互调用;
- Base64
23. Java回调机制
讲了一个很好的小故事:
https://www.cnblogs.com/heshuchao/p/5376298.html#undefined - 类A的a()方法调用类B的b()方法;类B的b()方法执行完毕主动调用类A的callback方法。
回调的作用是让类B获取类A的引用和一些需要处理的信息(类A向类B注册),让类B处理完成后使用类A的引用调用类A的方法(回调,类B通知类A执行完毕)。
使用场景:
- 多线程的Runnable是一个回调接口;
24. 一个十进制的数在内存中是怎么存的?
以二进制补码的形式存储。
25. 为啥有时会出现4.0-3.6=0.40000001这种现象
因为二进制系统中无法精确地表示分数1/10,就像十进制无法精确表示分数1/3一样,计算机在计算10进制小数的过程中要先转换为2进制计算,这个过程中出现了误差。如果想解决这个问题可以使用BigDcimal类。
23 .在Java中,如何跳出当前的多重嵌套循环
- 使用标识法;
- 改变循环条件法;
- return;
- 抛出异常;
24. 为什么父类中的静态方法不能被重写?
静态方法是和类绑定的,类可以直接调用,非静态方法是对象的成员,存放在堆中,只能被对象调用。重写的目的在于根据创建对象的所属类型不同而表现出多态。因为静态方法无需创建对象即可使用,没有对象,重写所需要的对象所属类型这个要素不存在,因此无法重写。
Java常见集合
1. List和Set区别
- List 可重复,有顺序
- Set 不可重复,无顺序
2. Set和hashCode以及equals方法的联系
给Set里存入元素时必须使用hashCode和equals来存放,先根据对象的hashcode值来对比set集合中是否已存在与该元素hashcode值相等的元素,如果没有则直接存入,如果有则使用equals方法比较两个对象是否相等,若想等则不存入,否则存入;
3. List 和 Map 区别
- list是存储单列数据的集合,map是存储键值对这样的双列数据集合;
- List中存储的数据是有顺序的,并且允许重复;
- Map中存储的数据是没有顺序的,键不能重复,值可以重复;
- List实现了Collection接口,而Map本身就是接口
4. ArrayList和LinkedList的区别
- 区别:
- ArrayList是实现了基于动态数组的数据结构;LinkedList是基于链表结构;
- 对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针;
- 对于插入或删除操作,LinkedList比较占优势,因为ArrayList要移动数据;
- 优缺点:
- 如果向末尾插入元素,对两者来说是一样的,如果向中间插入元素,ArrayList需要移动数据,而LinkedList只需重新指向两个指针;
- 删除同理,LinkedList占优势;
- LinkedList集合不支持高效的随机访问,因为可能产生二次项行为;
- ArraList的空间浪费主要体现在list列表的结尾预留一定的容量空间,而LinkedList的空间花费主要体现在他的每一个元素都需要消耗相当的空间,因为每个元素都有指针;
5. 数组和集合的区别
- 数组的长度是固定的;集合的长度是可变的;
- 数组既可以存储基本数据类型,也可以存储引用数据类型;集合只可以存储引用数据类型;
- 数组只能存储同种数据类型的元素;集合可以存储不同类型的元素;
7. ArrayList和Vector的区别
- ArrayList是线性不安全的,不同步,但是性能较好;
- Vevtor是线性安全的,同步,性能较差;
数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?
- Array可以包含基本类型和对象类型,ArrayList只能包含对象类型;
- Array大小是固定的,需要事先确定好大小,ArrayList大小是动态变化的,可以进行扩容。
- Array存储的一定是同种类型的元素,而ArrayList可以储存不同类型的对象。
ArrayList:JDK1.6ArrayList初始默认容量为10,扩容规则:扩容后的大小=原始大小+原始大小/2 +1;1.7每次扩容1.5倍;
8. HashMap 和 Hashtable 的区别
- 继承不同:hashtable继承Dictionary ;hashmap继承AbstractMap;
- hashtable是线性安全的;hashmap是非线性安全的;所以hashmap效率高于hashtable,但安全性低;
- hashtable的key和value都不允许是null值;hashmap的键可以为唯一null,value可以有一个或多个为null;
- 遍历方式不同:hashtable使用的是Enumeration,hashmap使用的是Iterator;
- 计算哈希值的方法不同:
- hashtable直接使用对象的hashcode,hashcode是根据对象的地址或字符串或数字计算出来的int值,探后使用除留余数法来获得最终位置;
- hashmap为了提高计算效率,将哈希表的大小固定了2的幂,这样在取模运算时不需要做除法,只需要做位运算,效率较高;、
- 初始容量和扩容方式:
- hashtable初始长度为11,之后每次扩容变为11*2+1;
- hashmap默认初始长度为16,之后每次扩充为原来的二倍;
- 如果创建时给大小,hashtable会直接使用这个大小,而hashmap会将其扩容为2的幂次方大小;
9. HashSet 和 HashMap 区别
- hashset实现了set接口;hashmap实现了map接口;
- hashset仅仅存储单列对象,hashmap存储键值对;
- hashset使用add放入元素,hashmap使用put放入元素;
- hashset使用成员变量来计算hashcode;hashmap使用键对象来计算hashcode值;
- hashmap相对于hashset较快,因为它使用唯一的键获取对象,hashset较hashmap来说较慢;
10. HashMap 和 ConcurrentHashMap 的区别
- ConcurrentHashMap1.7之前采用数组(segment)+数组(hashEntry)+链表(hashEntry);1.8之后采用数组+链表/红黑树;
- ConcurrentHashMap是线性安全的,采用分段锁,将segment的每个桶锁起来,而hashtable是将整张表锁起来,相当于HashTable的替代,但是比hashtable的扩展性好,并发性高;
- hashmap允许key和value同时为null,而ConcurrentHashmap不允许;
- hashmap不允许通过Iterator便利的同时对元素进行修改,而ConcurrentHashMap允许该行为,并且该更新后续的遍历可见。
11. HashMap 的工作原理及代码实现,什么时候用到红黑树
12. 多线程情况下HashMap死循环的问题
13. HashMap出现Hash DOS攻击的问题
构造字符串,让它们的hashcode值相等,不断向hashmap中添加这样的值,使hashmap退化为线性表,导致性能下降;
14. ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
ConcurrentHashMap工作原理
15. 手写简单的hashmap源码
16. Arraylist和Linklist默认空间是多少?
- HashMap;初始化大小是16,扩容因子默认为0.75,扩容机制:当前大小超过容量乘扩容因子的大小时进行扩容,扩容长度为2的幂次方倍
- ArrayList:JDK1.6ArrayList初始默认容量为10,扩容规则:扩容后的大小=原始大小+原始大小/2 +1;1.7每次扩容1.5倍;
- LinkedList:LinkedList是一个双向链表,没有初始化大小,也没有扩容机制;
17. hashmap解决冲突的方法
https://blog.csdn.net/mashaokang1314/article/details/88672035
18. Object的hashcode()是怎么计算的?
根据一定的规则将与对象相关的信息(比如对象的存储地址、对象的字段等)映射成一个数值,这个数值称为散列值,底层是通过C++来实现计算的。
19. 为什么要重写hashcode()和equals()以及他们之间的区别与关系;
- equels方法默认情况下比较的是两个对象的地址,由于判断效率较低,一般判断的时候先判断两个对象的hashcode,如果相同再用equals判断,如果只重写equels而不重写hashcode,会造成hashcode的值不同,而equels方法判断出来结果为true。
20. Java Collections和Arrays的sort方法默认的排序方法是什么
- Conllections的sort方法采用归并排序;
- Arrays中的sort方法对于基本数据类型数组采用快排,对于引用数据类型数据采用归并;
进程和线程
1. 线程和进程的概念、并行和并发的概念
-
进程是具有一定能够独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位,个人理解为正在运行中的一个程序,也可以理解为一个程序的执行过程;
-
线程是进程的一个实体,是CPU调度和分派的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源。
-
两者的关系:线程是进程中的不同执行路径,在同一个进程中,线程的切换不会引起进程的切换,在不同进程中的线程进行切换时,会引起进程的切换;
比如:打开电子邮件箱,这就是一个进程,在里面的发送、接收邮件等过程相当于多个线程; -
区别:
- 进程可以独立执行,线程不能独立执行,必须依存在应用程序中;
- 切换时的资源开销:进程的每次调度开销较大;线程切换时,仅需保存和设置少量寄存器内容。线程的切换、同步、通信都无需操作系统内核的干预;
- 拥有的资源:进程可以拥有资源,线程本身并不拥有系统资源,而是仅有一点能保证独立运行的资源;
- 独立性:在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多;因为进程之间的资源是独立的,线程之间的资源是共享的;
- 支持多处理机系统:对于单线程进程,即使再多处理机中也只能运行在一个处理机上,对于多线程进程,可以分配到多个处理机上;
-
并行:同一时刻有多个进程或线程在执行;
-
并发:同一时间内,有多个进程或线程来换切换执行;
2. 创建线程的方式及实现
-
继承Thread类创建线程:
- 定义Thread类的子类,并重写该类的run方法,创建该类的实例就是创建线程对象;
-
实现Runable接口来创建:
- 定义Runnable接口的实现类,并重写该接口的run方法,以该类的对象作为Thread类的target参数来创建对象;
-
使用Callable和Future创建:
- 创建Callable接口的实现类并实现call方法,且该方法有返回值;
- 使用FutureTask类来包装Callable对象,该Future对象封装了call方法的返回值;
- 使用FutureTask对象作为Thread的target参数创建线程;使用get方法来实现线程类;
-
三种方式优缺点的对比:
-
Runable和Callable归为一类:
- 线程类只是实现了接口,还可以继承其他类;
- 在这种方式下多个线程可以共享一个target对象,非常适用于多个线程来处理同一份资源的情况,较好的体现了面向对象的思想;
- 编程较为复杂;
-
采用Thread尅类方式创建:
- 编写简单,可以直接使用this获得当前线程对象;
- 不能再继承别的类;
-
3. 进程间通信的方式
-
传统的线程通信:借助Object类的wait()、notify()和notifyAll来实现;
- wait():导致当前线程等待,直到其他线程调用该同步监视器的notify或notifyAll方法来唤醒;调用该方法,当前线程会释放对该同步监视器的锁定。
- notify():唤醒在此同步监视器上等待的单个线程,若有多个线程等待则随机唤醒一个,只有当前线程放弃对同步监视器的锁定后,才可以执行被唤醒的线程;
- notifyAll():唤醒在此同步监视器上等待的素有线程,只有当前线程放弃对同步监视器的锁定后,才可以执行被唤醒的线程;
-
使用Condition控制线程通道:当使用Lock对象来保证同步时,系统中不存在隐式同步监视器,Java提供了一个Contidition类来保持协调,它为每个对象提供了多个等待集。
- await:类似于隐式同步监视器的wait方法;
- signal:类似于notify方法;
- signlAll:类似于notifyAll方法;
-
使用阻塞队列(BlockingQueue)控制线程通信:当生产者线程试图向队列中放入元素时,如果队列已满,则该线程阻塞;当消费者线程从队列中拿出元素时,如果该队列为空,则该线程阻塞;
4. 说说 CountDownLatch、CyclicBarrier 原理和区别
参考:CountDownLatch、CyclicBarrier线程计数器
- CountDownLatch计数器只能使用一次,而CylicBarrier的计数器可以使用reset方法重置。
- CyclicBarrier还有其他有用的方法,如getNumberWaiting获得阻塞线程的数量;
- CountdownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程;
5. 说说 Semaphore 原理
- Semaphore字面意思为信号量,初始化对象是给定一个整数A,表示允许活动的最大线程数,每个线程进入时先调用acquice方法,判断当前A是否等于0,如果不等于0则进入执行,并且A-1;如果等于0则阻塞,等待里面执行完的一个线程会调用release方法释放一个许可,这时A+1,再从阻塞线程中允许一个进入。
- 比较典型的问题是停车问题:停车位只有3个,而现在有5个车,semaphore相当于门卫,进入3个车之后,semaphore不在允许再辆进入,其他的车只能等着什么时候有车辆出来腾出空位才能进去。
- 和synchronized相比,synchronized只允许一辆车进入,Semaphore可以允许指定个数的车进入;
6. Exchanger的实现原理
Exchanger是一个用于线程间协作的工具类,用于进行线程间的数据交换;它提供了一个同步点,到达这个同步点的两个线程可以通过exchange方法交换数据,若有一个线程提前到达,但是当前同步点并没有线程等待,则它就会阻塞等待,直到下一个线程到达同步点,两线程交换数据。因此该工具类的线程对象是成对的;多用于两线程的数据交换。
7. ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
参考文章:ThreadLocal为什么会出现OOM
-
ThreadLocal 原理分析:每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有变量。ThreadLocal为每个线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
-
OOM内存溢出情况:线程池的一个线程使用完ThreadLocal对象之后,再也不用,由于线程池不会退出,线程池中的线程都存在,同时ThreadLocal变量也会存在,占用内存,导致OOM溢出。
-
实际上每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object;由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
8. 讲讲线程池的实现原理
线程池在系统启动时即创建大量空闲线程,程序将一个Runnable对象或Callable对象传给线程池,线程池会启动一个线程来执行他们的run或call方法,当方法执行结束后,线程并不会死亡,而是再次返回到线程池成为空闲状态,等待执行下一个Runnable对象的方法,线程池还可以有效地控制系统中并发线程数量。
- 线程池的几个重要参数
corePoolSize 规定线程池有几个线程在运行;
maximumPoolSize 当workQueue满了,不能添加任务的时候,这个参数才生效,规定线程池最多只能有多少个参数;
keepAliveTime:超出线程池数量的线程,如果长时间没有执行,超过keepAliveTime时就会消亡;
unit:生存时间对应的单位;
workQueue:存放任务的队列;
threadFactory:创建线程的工厂;
handler:当workQueue已经满了,并且线程池数已经达到了maxmumPoolSize时将执行的决绝策略。
- 任务提交后的流程分析
- 判断当前运行的数量是否超过corePoolSize ,如果没有就创建一个线程来执行任务;
- 如果超过就将任务加入到workQueue中;
- 如果workQueue已经满了,就检查当前运行线程是否超过maximunPoolSize,如果没超过就直接创建一个线程运行,否则就拒绝这个任务的提交。
- 使用线程池的优势:
- 线程池能够对线程进行统一分配,调优和监控
- 降低资源消耗;
- 提高响应速度;
- 提高线程的可管理性;
任务拒接策略有哪几种?
- ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常;
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常;
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务;
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
9 . 线程池的几种实现方式
- newCachedThreadPool:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中;
- newFixedThreadExecutor:创建一个可重用、具有固定线程数的线程池;
- newSingleThreadExecutor:创建一个只有单线程的线程池;
- newScheduledThreadPool:创建具有指定数量的线程池,它可以在指定延迟后执行线程任务;
10. 线程的生命周期,状态是如何转移的
- 新建:当线程对象创建后就进入了新建状态;
- 就绪:当调用对象的start方法后进入就绪状态,随时等待cpu的调度;
- 运行:当cpu开始调度处于就绪状态的线程,开始进入运行状态;
- 阻塞:处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞;
- 死亡:线程执行完了,或因异常退出了run方法,结束生命周期;
锁机制
1. 说说线程安全问题,什么是线程安全,如何保证线程安全
-
线程安全的概念:当多个线程访问某一个类时,这个类始终能表现出正确的行为,那么这个类就是线程安全的。当多线程访问时,采用加锁机制,每次只能有一个线程访问数据,别的线程只能等待,直到当前线程执行完毕。
-
采用同步代码块;
-
采用CAS算法;
2. 重入锁的概念,重入锁为什么可以防止死锁
- 重入锁是指重复获得资源的锁,已经获得锁的线程可以对当前的资源重入加锁而不会引起阻塞;不可重入锁是不可重复获得资源的锁,当已经获得锁的线程对当前资源再次加锁时,会把自己阻塞。
- 当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
- 防止死锁:假如有1个线程T获得了对象A的锁,那么该线程T如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样的话就会出现死锁的情况。
- 重入锁最大的作用就是防止死锁;
3. 产生死锁的四个条件(互斥、请求与保持、不剥夺、循环等待)
- 互斥条件:线程要求对所分配的资源进行排他控制,即在某段时间内,某资源仅为一个线程所占有,其他请求该资源的线程只能等待;
- 不可剥夺条件:线程未执行完之前,其所占有的资源不能被其他线程剥夺;
- 请求与保持条件:线程未拥有运行所需的全部资源,而缺失资源被其他线程占有。
- 循环等待条件:存在一种线程资源等待链,当前每一个线程所占有的资源,都是下一个线程运行所需的资源;
预防死锁 - 破坏请求与保持条件:对一个线程在请求资源时,他不能持有不可剥夺资源;
- 破坏不可剥夺条件:对一个已经保持某些不可剥夺资源的线程,提出新的资源而不能得到满足时,他必须释放已经保持的所有资源,待以后需要时在重新申请;
- 破坏循环等待条件:对系统所有资源类型进行线性排列,并赋予不同序号。规定每个线程必须按序号递增的顺序请求资源;
常用的避免死锁方法 - 一次封锁法:每个线程将所有要使用的数据全部加锁,否则,就不能继续执行;
- 顺序封锁法:预先对数据对象规定一个封锁顺序,所有线程都按这个顺序加锁;
- 银行家算法:保证线程处于安全线程序列;
有助于最大限度降低死锁的方法 - 按同一顺序访问对象;
- 避免事务中的用户交互;
- 保持事务简短并在一个批处理中;
- 使用低隔离级别。
4. volatile 实现原理(禁止指令重排、刷新内存)
volatile修饰符的实现原理
5. synchronized 实现原理(对象监视器)
https://blog.csdn.net/mashaokang1314/article/details/88750509
synchronized的对象锁,其指针指向的是一个monitor对象的起始地址,每个对象实例都会有一个monitor,monitor可以与对象一起创建,销毁,或者当线程试图获取对象锁时自动生成;
- _WaitSet是用来保存每隔等待锁的线程对象;
- _owner指向持有ObjectMonitor对象的线程;
当多个线程同时访问一段同步代码时,会先存放到_EntryList集合中,接下来当线程获取到对象的monitor时,就会把_owner设置为当前线程。同时count+1,如果线程调用wait方法,就会释放当前持有的monitor,那么_owner就会被设置为null,同时count-1,并且该线程进入WaitSet集合中,等待下一次被唤醒。
6. synchronized 与 lock 的区别
- synchronized是java内置关键字,在jvm层面,而Lock是个java类;
- synchronized无法判断是获取到锁的状态,lock可以判断是否获到锁;
- synchronized会自动释放锁,而lock需要在finally手工释放锁;
- synchronized,如果线程1获得锁,则线程2会一直等待下去;lock,如果线程1获得锁,线程2尝试获取不到,可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平;lock锁可重入、可判断、可公平;
- lock适合含大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题;
9. AQS的同步队列
- 概念:同步器AOS内部的实现是依赖同步队列来完成同步状态的管理,单钱线程获取同步状态失败时,同步器AOS会将当前线程和等待状态等信息构造为一个节点加入到同步队列,同时会阻塞当前线程;当同步状态释放的时候,会把首节点中的线程唤醒,再次尝试获取同步状态。
- 具体实现:在获取同步状态时,同步器维护一个同步队列,获取失败的线程会被加入到队列中并在队列中自旋;移除队列的条件是前驱节点为头结点并且获取到了同步状态。在释放同步状态时,同步器会调用tryRelease方法释放同步,然后唤醒有节点的后继结点。
- 分类:独占锁和共享锁
- 独占锁又分为公平锁和非公平锁,公平锁时按照FIFO原则从队列中获取线程,而非公平锁是不管等待时间的长短,直接获取;
- 共享锁:同一个时候能够被多个线程获取的锁;如:semaphore
10. CAS无锁的概念、乐观锁和悲观锁
11. 什么是ABA问题,出现ABA问题JDK是如何解决的
这个问题是由CAS引出的。
共享变量为1000元
线程A对1000元进行操作,线程B取走了500元,此时主存中还有500,线程C又放入了500元,此时主存中1000元。这时候线程A想向主存中放入更改后的变量,发现原来的值并没有发生改变,还是1000,操作成功。其实已经发生了改变(减了又加)。
优化:版本号对比。一个数据一个版本,即使数据一样版本号也不一样,所以也不能修改成功。
version方式:一般是在共享变量上加上一个数据版本号version,表示变量被修改的次数,当变量被修改时,version加1。这样可以防止ABA问题的发生。
12. 偏向锁、轻量级锁、重量级锁、自旋锁的概念
https://blog.csdn.net/mashaokang1314/article/details/88750509
Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
- 偏向锁:当只有一个线程访问同步代码块时会被加上偏向锁,直到有别的线程来访问的时候,偏向锁会升级为轻量级锁;
- 自旋锁:如果有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等持(进行无意义的循环)有锁的线程释放锁。避免了用户线程和内核得切换的消耗;会有时间阈值,自旋时间超过这个值,就会阻塞。
- 轻量级锁:线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
- 重量级锁:重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
单例模式的实现双检锁
package com.westos.Demo;
public class Singleton2 {
private volatile static Singleton2 singleton2;
private Singleton2(){}
public static Singleton2 getSingleton2(){
if(singleton2==null){ //第一次检查
synchronized (Singleton2.class){
if(singleton2==null){ //第二次检查
singleton2=new Singleton2();
}
}
}
return singleton2;
}
第一次检查为了避免来回的加锁解锁,节省开销。只有当对象第一次创建时,才会进行同步。
第二次检查时为了保证只创建一个对象。
使用volatile的作用是为了避免创建对象时代码重排序。
创建对象分为三步:
- 分配内存空间;
- 初始化对象;
- 引用对象;
2,3步都依赖第一步,所以第一步不会重排,但是2、3步并没有直接联系,所以可能会发生重排序问题。比如1 3 2 这时候如果在并发情况下,线程A执行完第3步 引用对象,这是线程B到达了第一次检查if(singletn2==null)
发现并不成立直接return singleton2,而这时步骤2还没有执行,对象还没有初始化,返回的是一个不完整的对象。而如果加上volitile就不会出现重排序问题,因为volitile规定了写操作(初始化)必须先行于读操作(引用)之前。