参考链接
https://blog.csdn.net/fangchao2011/article/details/89203535
https://blog.csdn.net/weixin_43495390/article/details/104034471
https://blog.csdn.net/weixin_43495390/article/details/86533482
spring:https://blog.csdn.net/a745233700/article/details/80959716
Java
· JDK 和 JRE 有什么区别?
-
JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的
开发环境和运行环境
。 -
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了
所需环境
。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
· JAVA中的几种基本类型,各占用多少字节?
-
char的包装类是Charator,int的包装类是Integer,使用集合时只能用包装类作为泛型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-keVOyqkP-1631800271987)(en-resource://database/610:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFF0l9QA-1631800271990)(en-resource://database/611:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQ0vYYD7-1631800271993)(en-resource://database/614:1)]
· String能被继承吗?
不可以
,因为String类有
final修饰符
,而final修饰的类是不能被继承的。平常我们定义的String str=“abc“(直接赋一个字面量)和String str=new String(“abc”)(通过构造器构造)还是有差异的。
String str=“abc”和String str=new String(“abc”); 产生几个对象?
1.前者1或0,后者2或1,先看字符串常量池,如果字符串常量池中没有,都在常量池中创建一个,如果有,前者直接引用(栈),后者在堆内存中还需创建一个“abc”实例对象(堆)。
2.对象的引用存储在栈当中,如果是编译期已经创建好(直接赋值字符串)的就存储在常量池中,如果是运行期(new出来的或含有变量的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
3.为了提升jvm性能和减少内存开销,避免字符的重复创建,项目中还是不要使用new String去创建字符串,最好使用String直接赋值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aLhkj9ir-1631800271997)(en-resource://database/616:1)]常量池:在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIGNVMr1-1631800271998)(en-resource://database/621:2)]
存放位置:之前在方法区(永久代中),后来放在堆当中,Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。
元空间内的规则:元空间中类及其相关的元数据
和类加载器
生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。
拼接字符串
分为变量拼接和已知字符串拼接,只要拼接内容存在变量,那么该拼接后的新变量就是在堆内存中新建的一个对象实体。String str = “abc”;//在常量池中创建abc
String str1 = “abcd”;//在常量池中创建abcd
String str2 = str+“d”;//拼接字符串,此时会在堆中新建一个abcd的对象,因为str2编译之前是未知的
String str3 = “abc”+“d”;//拼接之后str3还是abcd,所以还是会指向字符串常量池的内存地址
System.out.println(str1
str2);//false
System.out.println(str1
str3);//true
· String, Stringbuffer, StringBuilder 的区别
String 字符串常量(final修饰,不可被继承,因为是常量所以线程安全),String是常量,当创建之后即不能更改。
StringBuffer 字符串变量(线程安全,效率低),也是final类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的Append方法也做了同步处理(synchronized修饰)。其toString方法会进行对象缓存,以减少元素复制开销。
StringBuilder 字符串变量(非线程安全,效率高),与StringBuffer一样都继承和实现了同样的接口和类,方法除了没使用synch修饰以外基本一致,不同之处在于最后toString的时候,会直接返回一个新对象。
少量数据:string 单线程大量数据:StringBuilder 多线程:StringBuffer
· == 和 equals 的区别是什么?
- ==:对基本类型,比较值是否相同;对对象,比较引用是否相同
- equals:本质上就是==,只是对于String和Integer等重写了该方法,把它从应引用比较改成了值比较
· Java中的全局变量
Java中不存在全局变量,但可以使用static来表示一个伪全局的概念
· 讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候, 他们的执行顺序。
类加载器实例化时进行的操作步骤(加载–>连接->初始化)。父类静态代变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量(父类实例成员变量)、父类构造函数、子类非静态变量(子类实例成员变量)、子类构造函数。
· 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。
1、实例化:抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类
继承
,接口要被类
实现
。
3、方法:接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
4、变量:接口里不能有变量,只能有公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现。abstract void abc();不能写成abstract void abc(){}。
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
· comparator 和 comparable 有什么区别?
Comparable 是排序接口,由需要进行排序的类来实现,在类的内部定义方法实现排序。
Comparator 是比较器接口,可以对没有定义排序规则的类按照一定的比较规则进行排序,在类的外部实现排序。
· 动态代理 https://blog.csdn.net/HEYUTAO007/article/details/49738887
AOP的拦截功能是由java中的动态代理来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执行。不同的切入时机对应不同的Interceptor的种类)
AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。jdk动态代理是由java内部的
反射机制
来实现的,cglib动态代理底层则是借助
汇编语言asm
来实现的。
总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
-
JDK动态代理:
让target类和代理类实现同一接口,代理类持有target对象
,来达到方法拦截的作用,这样通过接口的方式有两个弊端,一个是必须保证target类有接口,第二个是如果想要对target类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明,实现上略微有点限制:
核心:Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(), this); -
cglib动态代理:优秀的动态代理框架,它的底层
使用ASM在内存中动态的生成被代理类的子类
,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理:
cglib有两种可选方式,
继承和引用
。第一种是基于继承实现的动态代理,所以可以直接通过super调用target方法,但是这种方式在spring中是不支持的,因为这样的话,这个target对象就不能被spring所管理,所以cglib还是才用类似jdk的方式,
通过持有target对象来达到拦截方法的效果
。
CGLIB的核心类:
net.sf.cglib.proxy.Enhancer – 主要的增强类
net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用,如使用:
Object o = methodProxy.invokeSuper(proxy, args);//虽然第一个参数是被代理对象,也不会出现死循环的问题。
· final 的用途
final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改
· 写出三种单例模式实现。
懒汉模式、饿汉模式、静态内部类模式
-
懒汉模式,线程不安全,在使用的时候才进行单例的初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTJYEjyr-1631800271999)(en-resource://database/639:1)] -
饿汉模式,在装载类时就进行类实例化,基于classloder机制避免了多线程的同步问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdtRNDxT-1631800272000)(en-resource://database/640:1)] -
静态内部类,同样利用了classloder的机制来保证初始化instance时只有一个线程,这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZzApEeq-1631800272001)(en-resource://database/641:1)]
· 如何在父类中为子类自动完成所有的 hashcode 和 equals 实现?这么做有何优劣。
同时复写hashcode和equals方法,优势可以添加自定义逻辑,且不必调用超类的实现。
· 谈谈访问修饰符 public、private、protected、default 在应用设计中的作用
访问修饰符,用于对象变量方法的封装,主要标示修饰块的作用域,方便隔离防护
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o01wmIQZ-1631800272003)(en-resource://database/642:1)]
public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
default:即不加任何访问修饰符,通常称为”默认访问模式”。该模式下,只允许在同一个包中进行访问。
· 泛型的存在是用来解决什么问题。
泛型的本质是
参数化类型
,也就是说所操作的数据类型被指定为一个参数,包括
泛型类、泛型接口、泛型方法。泛型的好处是1)支持泛化,比如arraylist的对象定义就是泛型 2)在编译的时候检查类型安全,因为不用泛型可能需要强制类型转换,而强制转换可能是不安全的,如果用泛型在编译器就能发现错误 3)所有的强制转换都是自动和隐式的,以提高代码的重用率
· 序列化和反序列化
Java序列化就是指把Java对象转换为字节序列的过程 Java反序列化就是指把字节序列恢复为Java对象的过程。
序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)
· 写一个字符串反转函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZoVE6J5-1631800272004)(en-resource://database/644:1)]
· String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
· java 中 IO 流分为几种?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
· Files的常用方法都有哪些?
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
对象拷贝
· 深拷贝和浅拷贝区别
对基本类型,深拷贝浅拷贝都是一样的
对引用类型(对象),简单赋值是浅拷贝,只是在栈中新建了引用,其堆指向的对象地址是一样的 ,因此拷贝了对象后,改变原对象中的变量值
会
改变拷贝的对象的值;深拷贝是连复制的对象中的其他变量也一并在堆中新建对象,拷贝了对象后,改变原对象中的变量值
不会
改变拷贝的对象的值
· 引用
java语言中为对象的引用分为了四个级别,分别为:强引用 、软引用、弱引用、虚引用。
强引用:默认声明,只要强引用存在就不会回收引用的对象
软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象
弱引用:对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)
内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。
异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JYWZ6pOO-1631800272005)(en-resource://database/643:1)]
java.lang.Throwable是所有异常的根
java.lang.Error是错误信息
java.lang.Exception是异常信息
1 Exception
一般分为Checked异常和Runtime异常,所有RuntimeException类及其子类的实例被称为Runtime异常,不属于该范畴的异常则被称为CheckedException。
-
CheckedException
只有java语言提供了Checked异常,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显示处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。这体现了Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种
1 当前方法知道如何处理该异常,则用try…catch块来处理该异常。
2 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
常见的checked异常:
Java.lang.ClassNotFoundException 找不到类
Java.lang.NoSuchMetodException 找不到方法
java.io.IOException io -
RuntimeException
如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。
我们比较熟悉的RumtimeException子类:
Java.lang.ArithmeticException 数学错误
Java.lang.ArrayStoreExcetpion 将错误的对象类型存储到数组中
Java.lang.ClassCastException 类强制转换
Java.lang.IndexOutOfBoundsException 越界
Java.lang.NullPointerException 空指针
2 Error
当程序发生不可控的错误时,通常做法是通知用户并中止程序的执行。与异常不同的是Error及其子类的对象不应被抛出。
Error是throwable的子类,代表编译时间和系统错误,用于指示合理的应用程序不应该试图捕获的严重问题。
Error由Java虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。
· throw 和 throws 的区别?
throws是用来声明一个方法可能抛出的所有异常信息,throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理。而throw则是指抛出的一个具体的异常类型。
· final、finally、finalize 有什么区别?
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由
垃圾回收器
来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾。
· try-catch-finally 中哪个部分可以省略?
catch 可以省略。
原因:更准确地说,try只适合处理运行时异常,try+catch适合处理运行时异常+cheked异常。
如果你只用try去处理cheked异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,cheked异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
· try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
会执行,在 return 前执行
容器/集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVhb3SPN-1631800272006)(en-resource://database/691:1)]
· List、Set、Map 之间的区别是什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wjUNUQc-1631800272007)(en-resource://database/645:1)]
· Collection 和 Collections 有什么区别?
- java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
- Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
· 哪些集合类是线程安全的?
vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
stack:堆栈类,先进后出。
hashtable:就比hashmap多了个线程安全。
enumeration:枚举,相当于迭代器。
· 数组和链表的区别是什么?
-
优缺点:
需要快速访问数据,很少插入和删除元素,就应该用数组。
需要经常插入和删除元素你就需要用链表。 -
内存存储区别:
数组从
栈或堆
中分配空间(取决于创建数组的方式), 对于程序员方便快速,但自由度小。
链表从
堆
中分配空间, 自由度大但申请管理比较麻烦. -
逻辑结构区别:
数组必须事先定义
固定的长度
(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;
链表
动态地进行存储分配
,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
· ArrayList 和 LinkedList 的区别是什么?
ArrayList和LinkedList都实现了List接口,有以下的不同点:
1、ArrayList是基于索引的数据接口,它的底层是
动态数组
。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以
双向循环链表
的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
2、相对于ArrayList,
LinkedList的插入,添加,删除操作速度更快
,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
3、
LinkedList比ArrayList更占内存
,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
· ArrayList 和 Vector 的区别是什么?
Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
ArrayList比Vector快,它因为有同步,不会过载。
ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
· ArrayList 和Array的区别是什么?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小后不可变的,而ArrayList大小是可变的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。
· ArrayList 的扩容机制
在不指定长度时初始大小为10,当超过了可负载大小时,扩容到1.5倍
· 如何实现数组和 List 之间的转换?
List转换成为数组:调用ArrayList的toArray方法。
String[] sids = sList.toArray(
new String[sList.size()
]);
数组转换成为List:调用Arrays的asList方法。
· 在 Queue 中 poll()和 remove()有什么区别?
poll() 和 remove() 都是返回第一个元素,并在队列中删除返回的对象,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
· HashSet的底层实现?
HashSet底层是HashMap,HashSet的值存放于HashMap的key上,其value统一为PRESENT
· 用过哪些 Map 类,都有什么区别
接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap
- HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
-
Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
和HashMap区别:1)hashTable同步的线程安全的,而HashMap是非同步的非线程安全的,效率上比hashTable要高。2)hashMap允许空键值,而hashTable不允许 - LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的(按插入顺序排序),也可以在构造时带参数,按照访问次序排序(按指定规则排序)。
- TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
· HashMap 是线程安全的吗?并发下使用的 Map 是什么,他们内部原理分别是什么,比如存储方式, hashcode,扩容, 默认容量等。
hashMap是线程不安全的,HashMap是哈西停数组+链表+红黑树(当链表长度大于8时自动转为红黑树,查询效率
由O(n)变成O(logn)
)实现的,采用哈希表来存储的。允许null值和null键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OKJPgmry-1631800272009)(en-resource://database/632:1)]
使用什么方式来控制map使得hash碰撞概率小,而哈希数组占用空间又少?答案:好的hash算法和扩容机制。
-
扩容机制
哈系统数组初始化长度是length(最初
默认是16
),Load factor为负载因子(默认值是
0.75
),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),
扩容后的HashMap容量是之前容量的两倍
。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
步骤:使用新的数组代替已有的容量小的数组,遍历旧的数组取得每个元素,释放旧数组对象应用,然后重新计算每个元素在新数组中的位置,并将其放进去 -
确定哈希桶数组索引位置
Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SA42CNsT-1631800272010)(en-resource://database/633:1)] -
HashMap的put方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-um4OFp9c-1631800272011)(en-resource://database/617:1)]
①.根据键值key计算hash值得到插入的数组索引i
②.如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作(插入到链表头部位置);遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 -
线程安全性
可能会产生死锁 -
红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3UWrmKfE-1631800272012)(en-resource://database/785:1)]
· 有没有有顺序的 Map 实现类, 如果有, 他们是怎么保证有序的。
TreeMap和LinkedHashMap是有序的(TreeMap默认升序,LinkedHashMap则记录了插入顺序)
LinkedHashMap的节点结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXp1Kd07-1631800272013)(en-resource://database/697:1)]
· 迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
· Iterator 怎么使用?有什么特点?
Java中的Iterator功能比较简单,并且只能单向移动:(1) iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。(2) 使用next()获得序列中的下一个元素。(3) 使用hasNext()检查序列中是否还有元素。(4) 使用remove()将迭代器新返回的元素删除。
为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
对map的iterator:Iterator<Entry<String,String>> iter=map.entrySet().iterator();
· Iterator 和 ListIterator 有什么区别?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
NIO
· Java网络通信机制:Socket编程
套接字之间的连接过程可以分为四个步骤:服务器监听,客户端请求服务器,服务器确认,客户端确认,进行通信。
1)服务器监听:是服务器套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
2)客户端请求服务器:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描叙它要连接的服务器的套接字,指出服务器的套接字的地址和端口,然后就像服务器套接字提出连接请求。
3)服务器连接确认:是指当服务器套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端的套接字的描述发送给客户端。
4)客户端连接确认:一旦客户端确认了此描叙,连接就建立完成了,双方开始通信。而服务器套接字继续处于监听状态;继续接受其他网络套接字的连接请求。
· 同步与异步 阻塞与非阻塞
-
阻塞和非阻塞关注的是程序在
等待调用结果(消息,返回值)时的状态
。
如果应用层调用的是阻塞型I/O,那么在调用之后,应用层
即刻被挂起
,一直出于等待数据返回的状态,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。
如果应用层调用的是非阻塞I/O,那么调用后,系统内核会
立即返回
(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o47xZWG3-1631800272014)(en-resource://database/634:1)] -
同步与异步关注的是消息通信机制。阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那
系统内核获取到的数据到底如何返回给应用层
呢?这里不同类型的操作便体现的是同步和异步的区别。
对于同步型的调用,应用层需要
自己去向系统内核问询
,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。
而对于异步型的调用,
应用层无需主动向系统内核问询
,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMqRHBcK-1631800272016)(en-resource://database/635:1)]
· Java网路通信中的同步与异步 阻塞与非阻塞
BIO和NIO的本质区别就是阻塞和非阻塞。
- 阻塞:应用程序在获取网络数据的时候,如果程序传输数据很慢,那么程序就一直等待着,直到数据传输完成。
- 非阻塞:应用程序直接可以获取已经准备就绪的数据, 无须等待。
同步和异步一般是面向操作系统与应用程序对I/O操作的层面上区别的。
- 同步:应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备完毕。
- 异步:所有的IO操作交个操作系统处理,与我们应用程序没有直接关系,我们程序不需要关系IO读写,当操作系统完成了 IO读写的时候,会给我们应用程序发出通知,数据传输完毕。
在Java网络通信中,同步异步说的是Server服务器端的执行方式。阻塞和非阻塞说的就是接受数据的方式。
BIO为同步阻塞模式,NIO为同步非阻塞。
· BIO、NIO及AIO
所有的系统I/O都分为两个阶段:等待就绪和操作。等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快。
-
BIO:传统IO,
同步阻塞,面向流
,如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。一般使用多线程或线程池实现。 -
NIO:非阻塞IO,
同步非阻塞,面向缓冲区
,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。 -
AIO:异步IO,异步非阻塞,不但等待就绪是非阻塞的,就连数据从网卡到内存的过程(真正的IO操作)也是异步的。
BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
· NIO:利用事件模型单线程处理所有I/O请求
不同于BIO模型的无法进行有效中断的读写函数,NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
NIO三大核心原理:Channel通道、Buffer缓冲区、Selector选择器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCfGSKmX-1631800272017)(en-resource://database/636:1)]
-
Buffer缓冲区:被包装成NIO Buffer对象,并提供了相关的API,底层是一个
数组
- channel通道:类似双向流
- selector选择器:有时也叫多路复用器,可以管理一个或多个NIO通道,确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
一个线程对应一个选择器,一个选择器对应多个通道,每个通道对应一个缓冲区,选择器根据不同的事件在各个通道上进行切换。
事件驱动模型:我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
NIO由原来的阻塞读写(占用线程)变成了
单线程轮询
事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
-
NIO带来了什么:
事件驱动模型
避免多线程而死使用单线程处理多任务
非阻塞IO
基于缓冲区的传输,比基于流的传输更高效
IO多路复用
· Reactor模型
一般情况下,I/O 复用机制需要事件分发器。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:反应器Reactor和主动器Proactor。 Reactor模式是基于
同步I/O
的,而Proactor模式是和异步I/O相关的。
最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
-
Reactor模式结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTNaqrnj-1631800272018)(en-resource://database/637:1)] -
Reactor包含如下角色:
1 Handle 句柄;用来标识socket连接或是打开文件;
2 Synchronous Event Demultiplexer:同步事件多路分解器,简称事件分发器:由操作系统内核实现的一个函数;用于阻塞等待发生在句柄集合上的一个或多个事件;(如select/epoll;)
3 Event Handler:事件处理接口
4 Concrete Event HandlerA:实现应用程序所提供的特定事件处理逻辑;
5 Reactor:反应器,定义一个接口,实现以下功能:
1)供应用程序注册和删除关注的事件句柄;
2)运行事件循环;
3)有就绪事件到来时,分发事件到之前注册的回调函数上处理; -
业务流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJv5VQKF-1631800272019)(en-resource://database/638:1)]
反射
对于任意一个类,能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种
动态获取对象的类信息以及通过类信息动态调用对象的方法
的功能称为java语言的反射机制。
就是把java类中的各种成分映射成一个个的Java对象,得到class对象后反向获取student对象里的各种信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbytmWH7-1631800272021)(en-resource://database/618:1)]
-
反射类实例创建的三个方式:
对象.getclass
类名.class
class.forName() 最常用https://blog.csdn.net/fangchao2011/article/details/89203535 -
Class.forName 和 ClassLoader 区别:
前者在loadClass后
必须初始化
,而后者在目标对象被装载后
不进行链接
,这就意味这不会去执行该类静态块中间的内容。
JVM
https://blog.csdn.net/torian2137/article/details/84435577
https://blog.csdn.net/aijiudu/article/details/72991993
· JVM组成
三大组成部分:
类加载器、运行时的数据区、执行引擎
。
1. 类加载器
类加载步骤:
加载->链接->初始化
-
加载:查找并加载类的二进制数据至内存中,将其放在运行时的
方法区
内,然后在
堆区
创建一个java.lang.Class对象用来封装类在方法区内的数据结构。
类的加载的最终产品是位于堆区中的Class对象
,Class对象封装了类在方法区内的数据结构 ,并且向Java程序员提供了访问方法区内的数据结构的接口。类加载器
并不需要
等到某个类被“首次主动使用”时再加载它。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WeJqPSBw-1631800272022)(en-resource://database/623:1)]
加载机制:父亲委托机制,除了启动类加载器外,其余的类加载都有且只有一个父加载器,当Java程序请求加载器加载一个类时。加载器首先委托自己的父加载器去加载该类,如果父加载器能够加载,则由父加载器完成加载任务,否则才由该加载器本身来加载该类。
目的:
提高安全性,因为在此之下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替父加载器加载的可靠代码。
加载器类型:
Java虚拟机的:
启动类加载器:没有父加载器,负责加载核心类库,如java.lang/Object等。启动类加载器从系统属性class.path所指定的目录中加载类库,其实现依赖于底层操作系统,属于虚拟机实现的一部分,没有继承classloader类。
扩展类加载器:父加载器为启动类加载器,从ext.dirs系统属性指定的目录中加载类库,或从JDK安装目录的子目录下加载类库。他是纯Java类,是classLoader的子类。
应用类加载器:父加载器为扩展类加载器,从环境变量classpath或系统属性class.path中加载类,是用户自定义类加载器的默认父加载器,是纯Java类,是classLoader的子类。
用户自定义的:
java.lang.ClassLoader的子类
用户自定义的加载方式
加载器分类:如果有一个类加载器能够成功加载一个类,那么这个类加载器叫做
定义类加载器
,其他能成功返回这个对象的引用的类加载器(包括定义类加载器)都被称作
初始类加载器
。如loader1要加载一个类,他去请求了它的父加载器loader2加载,最后由loader2加载成功,则loader2为定义类加载器,loader1和loader2为初始类加载器。
* 加载器之间的父子关系实际上是加载器
对象之间的包装关系
而非类之间的继承关系,一对父子加载器可能是同一个类加载器的两个实例。例如下面loader1和loader2都是MyClassLoader类的实例,并且loader2包装了loader1,loader1是loader2的父加载器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRXVi0OP-1631800272023)(en-resource://database/625:1)]
* 运行时包:由同一
类加载器
加载的属于
相同包
的类组成了运行时包,只有属于统一运行时包的类才能互相访问包可见的类和类成员(默认访问级别),这样能够避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员,因为他们会由不同的类加载器加载,不属于同一运行时包。
-
链接:将读到内存的类的二进制数据合并到JVM运行环境中
校验:检测生成的字节码是否正确,主要包括以下内容:
类文件的结构检查:确保类文件遵循Java类文件的固定格式
语义检查:确保类符合Java语法规定
字节码检查:确保字节码(代表Java方法,由被称作操作码的单字节指令组成的序列,每个操作码后都跟着一个或多个操作数)可以被Java虚拟机安全地执行,即其是否有合法的操作数
二进制兼容的验证:确保相互引用的类之间协调一致,例如在worker类的a方法调用car类的b方法,JVM在验证worker类时会检查在方法区内是否存在car类的b方法,加入不存在(即worker和car类的版本不兼容)
准备:为静态变量分配内存,并将所有的
JVM默认值
赋给所有静态变量
解析:将符号引用转为直接引用,如worker中的a方法调用了car中的b方法,在worker的二进制数据中,包括了一个对car类的b方法的符号引用,有b方法的全名和相关描述符组成。在解析阶段,JVM会把这个符号替换成一个指针,该指针指向car类的b方法在方法去的内存位置,这个指针就是直接引用。 -
初始化:将
代码中定义的值
赋给静态变量、执行静态块
静态变量的(显示)初始化两种途径:1)在静态变量的声明处进行初始化 2)在静态代码块中进行初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VTSTnoT-1631800272024)(en-resource://database/624:1)]
类的初始化步骤(加载和链接之后执行):
如果存在直接的父类,并且父类还没有被初始化,那么先初始化直接父类(接口例外,在初始化一个接口时并不会初始化它的父接口;在初始化一个实现类时并不会闲初始化它实现的接口)
如果类中存在初始化语句则依次执行
2. 运行时的数据区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6tRFKYY-1631800272025)(en-resource://database/621:2)]
- 程序计数器:记录当前所执行的字节码的行号指示器
-
方法区/永久代:.class类信息(封装其的java.lang.Class对象放在堆中)
静态域:静态变量、静态方法、成员方法
常量池:早期是字符串常量和基本类型常量,现在字符串常量池存放在堆中
垃圾回收
:废弃常量和无用的类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ad2ayWWc-1631800272026)(en-resource://database/784:1)]
内存分配时间
:在编译时就需要知道存储要求 -
堆区:对象(new出来的,堆存放它本身,其引用放在栈中)、数组、实例变量
年轻代、老年代、永久代(即上面的方法区)
垃圾回收
:由垃圾处理器完成
内存分配时间
:在过程的入口处知道所有存储要求 -
栈区:每个线程一个运行时栈,由栈帧组成,当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作
栈帧—是Java方法执行的内存模型,每个方法被执行时都会创建一个战争,只有栈顶的栈帧是有效的,叫当前栈帧,这个栈帧所关联的方法称为当前方法。
栈帧组成:
1.局部变量表:基本类型变量数据(包括使用赋值初始化获得的数组,使用new获得的数组仍然是在堆中的)、对象的引用(对象本身在堆或常量池(对字符串)中)
2.操作数栈
3.动态链接:一般用于常量池与该栈帧所属方法的引用的链接
4.方法返回地址
帧数据:方法的所有符号
垃圾回收
:数据大小和生命周期是确定的,当没有引用指向数据/超过变量的作用域时,java会自动释放掉为该变量分配的内存空间
栈数据(基本类型的实际值)共享:
假设我们同时定义: int a = 3; int b = 3; 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
内存分配时间
:在实际运行时才能知道存储要求
3. 执行引擎
- 解释器:转化字节码
-
编译器:遇到重复的代码将字节码编译成本机代码,本机代码将直接用于重复方法的调用
中间代码生成器:生成中间代码
代码优化器:优化上面的中间代码
目标代码生成器:生成及其代码或本机代码
探测器:负责寻找被多次调用的方法 - 垃圾回收器:只回收由new关键字创建的对象
· JVM调优参数
堆性能:-Xms(程序启动时占用的内存大小) –Xmx(程序最大占用的内存大小)
两个参数大小一般相等,因为在实际生产环境中,一台服务器往往只会执行一个任务,
独占服务器意味着没有必要调整JVM大小,每次调整反而会加大开销,设置相等能够
避免频繁扩容和垃圾回收释放堆内存在成的系统开销
。
栈性能:–Xss(对每一个线程栈的性能调优参数,影响堆栈调用的深度)
https://blog.csdn.net/aijiudu/article/details/72991993
· 类的初始化的时间
JAVA程序对类的使用方式分为两种:主动使用及被动使用。只有在每个类或接口被Java程序“首次主动使用”时才初始化它们。
主动使用:
- 创建类的实例(new)
- 访问某个类或接口的静态变量,或者对该静态变量赋值(不包括final类型)
-
调用
该类或该接口中定义
的静态方法 - 反射(如 Class.forName(“com.shengsiyuan.Test”) )
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化(如调用ClassLoader类的loadClass方法加载 一个类,并不是对类的主动使用,不会导致类的初始化。)
但在此之前可以进行类加载,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误 ,类加载器必须在程序首次主动使用该类 时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那 么类加载器就不会报告错误
特殊情形:final 类型的类变量,如果在编译时(转成.class文件)就可以确定,那么这个类变量就相当于“宏变量”,编译时,直接替换成值。所以,即使使用这个类变量,程序也不会导致该类的初始化!!—-相当于直接使用 常量
小模块:JVM的GC(堆区为主)
· 垃圾回收概念
1 栈区垃圾回收:数据大小和生命周期是确定的,当没有引用指向数据/超过变量的作用域时,java会自动释放掉为该变量分配的内存空间
2 堆区域垃圾回收:新生代(Eden、两个Surviror区(每次只有其中一个被使用))、老年代、持久区,
以此来优化垃圾回收的性能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nVS7IMdF-1631800272028)(en-resource://database/648:1)]
eden 和 survior 是按
8:1
分配的
-
原则:
新对象放到新生代Eden里
新的大对象直接放到老年代里
在Eden区第一次满时,会进行MinorGC,扫描eden区和from区,仍然存活的对象复制到to区(如果to空间不够,则直接进入老年代;新生代中的From空间中的对象每经过一轮gc都会涨一岁,当其岁数达到一定程度就会被移入老年代(或者岁数相等的对象超过from空间的一半时)也会进入老年代),然后清空eden和to,from和to区标签互换。当To区也被填满后,其中所有的对象都会移动到老年代中(不管是不是达标) -
MinorGC和MajorGC和FullGC:
MinorGC:新生代的gc,复制算法(复制->清空->互换)
步骤:eden、SurvivorFrom 复制到SurvivorTo ,年龄+1->清空eden和from区->from和to互换
时间:新生代Eden没有足够空间
MajorGC:老年代的gc,一般伴随着MinorGC,标记-清除算法
FullGC:新生代/老年代/原空间一起回收
时间:调用System.gc()、老年代满了(分配对象是大对象/To空间不够gc时直接把对象拷贝到老年代)
· 回收算法
- 标记清除算法:老年代,对象构成一棵树(可达性算法)。第一:标记从树根可达的对象,第二:清除不可达的对象。清除时要停止程序运行。否则产生新的对象是树根可达的,但没有被标记,会清除掉
- 复制算法:新生代,内存分成两块,空闲和活动。第一:标记可达对象,然后把对象复制到空闲区,将空闲区变成活动去,并把以前的活动区的对象清除掉并变成空闲区
- 标记整理算法:老年代,和标记清除算法类似,会将其进行碎片整理
可达性算法判断对象是否需要回收:
通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。其中,GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区域中的类静态属性引用的对象。
方法区域中常量引用的对象。
本地方法栈中JNI(Native方法)的引用的对象。
· 垃圾回收器种类
JVM中的垃圾回收器STW:在执行垃圾回收时,Java的其他线程都被挂起
- 串行Serial:复制算法
- parNew:多线程版的Serial
-
老年代垃圾回收器CMS收集器:垃圾回收四阶段 (CMS=并发、标记、清理):←实现的是标记清除算法,比较占用cpu资源,易造成碎片
初始标记:STW,判断哪些标记可以到达
并发标记:根据上次标记结果判断哪些不可到达、线程并发或交替执行
再次标记:STW,修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录
并发清理垃圾 -
G1:整体来看基于标记整理算法,局部来看基于复制,分代收集、空间整合
初始标记:STW,判断哪些标记可以到达
并发标记:根据上次标记结果判断哪些不可到达、线程并发或交替执行
最终标记:程序暂停
筛选回收
· 内存溢出和内存泄露
内存泄露:是指程序在申请内存后,无法释放已申请的内存空间
内存溢出:指程序申请内存时,没有足够的内存供申请者使用
· 出现了内存溢出要怎么排错
首先分析是什么类型的内存溢出,对应的调整参数或者优化代码。
-
老年代被占满:
代码内造成的可以通过分析工具进行分析、找出泄漏点
系统的打大并发和大对象造成的,需要优化代码或增加survior区的大小 - 持久代被占满:增加持久代空间的大小
- 内存被占满:无法创造新的线程
优化GC:
根据目前垃圾回收情况适当调整老年代、亲年代、持久代的比例
选择适合的垃圾收集器
实践:
用JDK自带的工具jvisualvm,安装插件Visual GC,dump下来堆信息,得到一个这样的hprof快照文件,使用mat来进行分析
· 什么情况下会发生栈内存溢出
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError异常
。 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出
OutOfMemoryError异常
。这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是 StackOverflowError 异常,而不会得到 OutOfMemoryError 异常。而在多线程环境下,则会抛出 OutOfMemoryError 异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cif1FDL1-1631800272029)(en-resource://database/646:1)]
内存溢出的简单测试法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWbMpAH0-1631800272030)(en-resource://database/647:1)]
多线程
http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-answers/
· 进程和线程的区别?
进程是操作系统进行资源分配的基本单位,可以被看作一个程序或者一个应用,线程是进程进行任务调度的基本单位,是在进程中执行的一个任务。线程需要相对较少的资源来创建和管理,并且能共享一个进程当中的资源。
· 什么时候用进程什么时候用线程?
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
· 多线程编程的好处?
提高并发性,CPU不会因为某个线程需要等待资源而进入空闲状态。
比多进程编程的好处:多个线程能共享一些基础数据比如堆内存,调度也比进程快,因此创建多个线程去执行一些任务回避创建多个进程更好。
· 用户线程和守护线程有什么区别
守护线程是一种特殊的线程,它是系统的守护者,在后台完成一些系统性的服务,比如垃圾回收线程
当我们在Java程序中创建一个线程,它就被称为用户线程。一个守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。
· 如何创造线程?
一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。
· 如何保证线程安全?
synchronized,使用原子类(atomic concurrent classes),锁,volatile,使用不变类和线程安全类。
线程安全主要在三个方面体现:原子性、可见性、顺序性,只有volatile不能保证原子性和顺序性
原子性:一个时刻只能有一个线程执行一段代码(锁、synchronize)
可见性:释放锁之前对共享数据做出的更改对之后要获得该锁的另一个线程是可见的(锁、synchronize、volatile)
顺序性:要能有效解决重排序(synchronize、happens-before
· 画一个线程的生命周期状态图。
新建,就绪,运行中,睡眠,阻塞,等待,死亡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnknw2KL-1631800272042)(en-resource://database/657:1)]
· 多线程阻塞的情况
调用sleep()方法使线程进入休眠状态,线程在指定时间内不会运行。
调用join()方法使线程挂起,使自己等待另一个线程的结果,直到另一个线程执行完毕为止。
调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态
· volatile关键字在Java中有什么作用?
使用volatile关键字去修饰变量的时候,所以线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。不能用来替代锁,因为其只保证数据可见性一致性,不能保证原子性
· synchronized原理
synchronized的底层就是锁机制,普通同步方法,锁是当前实例对象;静态同步方法,锁是当前类的class对象;同步方法块,锁是括号里面的对象
其他和可重入锁的原理类似,进入加1释放减1,通过监视器的enter和exit实现
· 锁相关
lock常用的则有ReentrantLock和readwritelock两者,添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。但lock需要手动释放锁,所以要把互斥区放在try内,释放锁放在finally内!!
-
读写锁:与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
适用于不经常修改单经常读取的数据
锁的结构:
共享锁变量、volatile、CAS、同步队列。
获取锁:
1、线程调用 CAS(threadId, 0, 1),预期threadId == 0, 若是符合预期,则将threadId设置为1,CAS成功说明成功获取了锁。
2、若是CAS失败,说明threadId != 0,进而说明有已经有别的线程修改了threadId,因此线程获取锁失败,然后加入到同步队列。CAS:比较与交换。若是内存值与期望值一致,说明没有其它线程更改目标变量,因此可以放心地将目标变量修改为新值。是原子操作,底层是CPU指令。
释放锁:
1、持有锁的线程不需要锁后要释放锁,假设是独占锁(互斥),因为同时只有一个线程能获取锁,因此释放锁时修改threadId不需要CAS,直接threadId == 0,说明释放锁成功。
2、成功后,唤醒在同步队列里等待的线程。
锁的全家福:
https://upload-images.jianshu.io/upload_images/19073098-af0e4145eee781bd.png
锁的种类:
-
公平锁/非公平锁:
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 -
可重入锁又名递归锁,是指在同一个线程获取某个锁后,在没释放前这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
可重入锁的意义在于防止死锁。实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。 -
独享锁/共享锁:
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。 -
乐观锁/悲观锁乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。在MySQL中就是MVCC
乐观锁代表:自旋锁、原子类CAS 悲观锁:其他 - 阻塞锁/自旋锁:阻塞锁获取锁失败后会阻塞,自旋锁无需阻塞
- 分段锁分段锁其实是一种锁的设计,并不是具体的一种锁,当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入
-
偏向锁/轻量级锁/重量级锁这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 - 自旋:尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
· 原子类
16种原子类,如使用原子的方式更新基本类型:
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
- int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
- boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
· synchronized和lock的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OWYePggB-1631800272043)(en-resource://database/619:1)]
· synchronized和volatile的区别
1.volatile本质:是java虚拟机(JVM)当前变量在工作内存中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前的变量,只有当前线程可以访问到该变量,其他的线程将会被阻塞。2.volatile只能实现变量的修改可见性,并不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。3.volatile只能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
· sleep和wait的区别
Sleep是休眠线程,使线程在一定的睡眠时间后继续执行,wait是等待,直接让该线程进入某个锁等待队列中
sleep是thread的静态方法,wait则是object的方法。
Sleep依旧持有锁,并在指定时间自动唤醒。wait则释放锁等待notify唤起
Sleep任何地方都可用,wait只能在同步块中使用
Sleep不需要捕获异常,wait需要用try-catch捕获异常
· start和run的区别
start()方法用来启动一个线程,run()方法是让线程执行线程体方法。start()方法来启动一个线程,真正实现了多线程运行,使用start后会进入就绪状态,线程调度器再选择其中的就绪线程进入run状态,而run方法是直接让线程进入执行状态,它只是线程中的一个函数而不属于多线程范畴
· 小模块:线程池
1 概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
2 工作机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3szeQOrw-1631800272044)(en-resource://database/783:1)]
-
作用:
节省创建线程和销毁线程的时间
、方便管理 - 使用场景:在一个任务中创建线程和销毁线程的时间远大于线程执行的时间
- 组成:四个组件–线程池管理器、工作线程(没有任务时处于等待状态)、任务接口(每个任务需要实现的接口,以供工作线程调度任务的执行)、任务队列
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
3 创建线程池两种方法
尽量使用ThreadPoolExecutor而不是Excutors类中的静态方法,从而避免使用Excutors类中的默认实现,比如允许请求队列的最大长度为Integer的max_value等,可能会堆积大量请求(singleThread和FixedThread)/创建大量线程(CachedThread和ScheduledThread)从而导致OOM
-
常见线程池/JDK中java.util.concurent中Excutors类中的静态方法:
①newSingleThreadExecutor单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
使用场景:需要保证顺序执行,并且只有一个线程在执行。
②newFixedThreadExecutor(n)固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
使用场景:已知并发压力的情况下,对线程数做限制
③newCacheThreadExecutor(推荐使用)可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,如果没有可以回收的了,又
智能的添加新线程
来执行。没有核心线程数。
使用场景:可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
④newScheduledThreadPool:支持线程定时操作和周期性操作。
使用场景:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50lZhfbi-1631800272046)(en-resource://database/780:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esuxD8NK-1631800272047)(en-resource://database/781:1)] -
ThreadPoolExecutor
-
相关参数
corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
maximumPoolSize:线程池允许的最大线程池数量
keepAliveTime:非核心线程的其他空闲线程的最大超时时间
unit:超时时间的单位
workQueue:工作队列,保存未执行的Runnable 任务
threadFactory:创建线程的工厂类
handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。 -
拒绝策略:
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,先将阻塞队列中的头元素出队抛弃,再尝试提交任务。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,因此不建议将该种策略配合优先级队列使用。
DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
4 线程池关闭:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4sMVlO1S-1631800272048)(en-resource://database/660:1)]
从上图我们看到线程池总共存在 5 种状态,分别为:
▫
RUNNING
:线程池创建之后的初始状态,这种状态下可以执行任务。
▫
SHUTDOWN
:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行结束。
▫
STOP:
该状态下线程池不再接受新任务,同时也不会处理工作队列中的任务,并且将会中断线程。
▫
TIDYING
:该状态下所有任务都已终止,将会执行 terminated() 钩子方法。
▫
TERMINATED
:执行完 terminated() 钩子方法之后。
关闭线程池的两种方法:
- shutdown:线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
- shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
优雅关闭线程池:shutdown->awaitTermination(这个方法可以等待工作队列中的方法结束)等待60s再尝试shutdownnow:清空工作队列,终止线程池中各个线程,销毁线程池
4 线程池四种工作队列/阻塞队列
有界队列
1、ArrayBlockingQueue 基于数组结构的
有界
阻塞队列
无界队列
2、LinkedBlockingQueue 基于链表结构的
无界
阻塞队列,FIFO (先进先出) 静态工厂方法
Executors.newFixedThreadPool
使用了这个队列
3、PriorityBlockingQueue 具有优先级的无限阻塞队列。
同步交移队列
4、SynchronousQueue 如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
只有在使用无界线程池或者有饱和策略时才建议使用该队列。
,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.
newCachedThreadPool
使用了这个队列。
5 线程池状态监控
ThreadPoolExecutor 也给出了相关的 API, 能实时获取线程池的当前活动线程数、正在排队中的线程数、已经执行完成的线程数、总线程数等。
· ThreadLocal:
线程内的局部变量,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
-
应用场景:解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。 -
原理:
每个线程都有个属性ThreadLocalMap用来维护该线程的多个ThreadLocal变量,该Map是自定义实现的entry[]数组结构并非继承自原生Map类,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是本地对象ThreadLocal,值是线程的变量副本(即实际的值)。
因此ThreadLocal其实只是个符号意义,本身不存储变量,仅仅是用来索引各个线程中的变量副本。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kzy0yMV0-1631800272049)(en-resource://database/658:1)]
通过threadlocalMap进行存储键值 每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。 -
内存泄露
1.为什么ThreadLocalMap使用弱引用存储ThreadLocal?
假如使用强引用,当ThreadLocal不再使用需要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的强引用,无法回收,造成内存泄漏。因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。
2.那通常说的ThreadLocal内存泄漏是如何引起的呢?
我们注意到Entry对象中,虽然Key(ThreadLocal)是通过弱引用引入的,但是value即变量值本身是通过强引用引入。
这就导致,假如不作任何处理,由于
ThreadLocalMap和线程Thread的生命周期是一致的
,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。导致内存泄漏。
3.如何解决?
(强制)
在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理
。一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。
ThreadLocal一般加static修饰,同时要遵循及时清理调用remove方法
。一方面ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费。另一方面在某些代码规范中遇到过这样一条要求:“尽量不要使用全局的ThreadLocal”实际上,这个解读都是不必要的,首先,静态ThreadLocal资源回收的问题,即使ThreadLocal本身无法回收,但线程中的Entry是可以通过remove清理掉的也就不会出现泄漏。第二种解读,多次复用值改变的问题,其实在调用remove后也不会出现。
· 使用线程安全的集合
任何集合类都可以通过使用同步包装器变成线程安全的,集合中的方法就会使用锁加以保护,提供线程安全的访问:
List synchArrayList = Collections.synchronizedList(new ArrayList());
但如果在其中使用了迭代器任然需要使用同步synchronized(synchHashMap)
· 并发和并行的区别
并行是两个或多个事件同时执行,在两个不同的实体/cpu上,并发指多个事件在一个小的时间片段内交替执行,在一个实体上
· 线程死锁
定义:因为线程之间的通信和资源获取冲突而导致所有线程都进入一个阻塞状态的不良现象。
死锁四条件:互斥、请求与保持、非抢占式、循环等待
解决死锁的方法:破坏掉其中一个条件
请求和保持–一开始就申请所有需要的资源
非抢占式–遇到申请资源又一时满足不了的,释放持有的所有资源
循环等待–确定资源申请顺序,只有拥有低等级的资源才能申请高等级的资源
恢复策略:强制停止某一进程、都回滚、强制剥夺某一进程资源给另一进程
小模块:内存模型
· 进程线程通信机制以及内存模型
线程通信机制:共享内存(隐式通信显式同步)和消息传递(显式通信隐式同步,使用getMeassage)
进程通信方式:管道、信号量、消息队列、共享内存、套接字
java线程通信java内存模型(JMM)控制:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(工作内存),本地内存中存储了该共享变量的副本(本地内存是JMM的一个抽象概念,并不真实存在。)
主内存:共享变量存储的区域即是主内存
工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ORQzkjVE-1631800272051)(en-resource://database/649:1)]
(java线程同步:Object类中wait()\notify()\notifyAll()方法)
· 重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
重排序分三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ekacyx4-1631800272052)(en-resource://database/650:1)]
其中第一个为编译器重排序,后两个是处理器重排序。
对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
· 内存屏障
内存屏障:
为了保障执行顺序和可见性的一条cpu指令
,比如指定load1装载的数据呀先于load2装载的顺序等。
happen-before:定义一个规则,即一个操作执行的结果需要对另一个操作
可见
(而不是一定要在其之前执行),那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。