文章目录
- 第一章 java基础
- 第二章 java集合
- **集合源码分析**
- 1,ArrayList
- 2, LinkedList
- 3, HashMap
- 4,ConcurrentHashMap1.7 源码分析
- 4.1 构造方法
- 4.2 [java中的Unsafe类](https://www.jianshu.com/p/cda24891f9e4)
- 4.3 [ConcurrentHashMap中的key与value为什么不能为null](https://www.cnblogs.com/zzq919/p/12978955.html)
- 4.4 [Integer的numberOfLeadingZeros方法解释](https://blog.csdn.net/SAN_YUN/article/details/84519800)
- 4.5 put源码
- 4.6 entryAt()方法
- 4.7 put方法
- 4.7 scanAndLockForPut()智能获取锁,而非lock()暴力获取
- 4.8 entryForHash
- 5,ConcurrentHashMap1.8 源码分析
- 6,Collections工具类
第一章 java基础
-
方法签名:要完整的描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名。返回值不是方法签名的一部分,也就是说不能有两个名字相同,参数类型也相同却返回不同类型值的方法
-
static修饰的方法可以被继承,但是不能重写。如果父类中有一个静态的方法,子类也有一个与其方法名,参数类型,参数个数都一样的方法,并且也有static关键字修饰,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法是看是哪个对象的引用;这种父子类方法不在存在多态的性质。
-
泛型擦除:Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
泛型的类型擦除原则是:- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
-
为什么在静态方法,静态初始化块或者静态变量的声明和初始化中不允许使用泛型?
- 静态变量是被泛型类所有实例所共享的。对于声明为MyClass的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass还是new MyClass创建的对象,都是共享一个静态变量。假设允许类型参数作为静态变量的类型。那么考虑下面一种情况
-
自动装箱与拆箱
- 装箱是调用了包装类的valueOf()方法,拆箱其实就是调用了xxxValue()方法
- java基本类型的包装蕾的大部分都实现了常量池技术,Byte,Short,Integer,Long这4种包装类型默认创建了数值[-128,127]的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回True Or False.
- 所有整型包装类对象之间值的比较,全部使用equals方法比较
- Java中有了基本数据类型,为什么还需要包装类型:
- Java是一个面向对象的语言,然而基本数据类型不具备面向对象的属性。当我们把基本数据类型包装成包装类型后,基本数据类型就具备了面向对象的属性。
- 在ArrayList 、HashMap这些容器来传输数据是,,基本类型int和double是传输不进去的,因为容器都是装泛型(object类型)的,所以需要转为包装类型进行传输。
-
在一个静态方法内调用一个非静态成员为什么是非法的?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问,而非静态成员是属于实例对象的,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员已经存在了,此时调用在内存中不存在的非静态成员,属于非法操作。
-
java接口中的成员变量为什么必须声明为public static final?
- java接口方法的默认修饰符是public abstract,接口中的变量的默认修饰符是public static final
- jdk8中,接口中可以有默认方法和静态方法,静态方法可以直接用接口名调用,实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
-
一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?
- 构造方法的主要作用是完成对类对象的初始化工作
- 如果一个类没有声明构造方法,也可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java就不会再添加默认的无参数构造方法了,这时候,就不能直接new一个对象而不是传递参数了。
-
equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
-
为什么重写equals时必须重写hashCode方法?
- 如果两个对象相等,则hashcode一定也是相同的。两个对象相等,对两个对象分别调用equals方法都返回true。但是,两个对象有相同的hashcode值,它们也不一定是相等的。因此,equals方法被覆盖过,则hashCode方法也必须被覆盖。
- hashCode()的默认行为是对堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象完全相同)
-
- 按值调用表示方法接收的是调用者提供的值,按引用调用表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
- java总是采用按值调用,也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给他的任何参数变量的内容。
- java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
- Java中方法参数的使用情况:
- 一个方法不能改变一个基本数据类型的参数(即数值型或布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
-
反射
- 何为反射?
-
反射机制允许程序在运行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法(反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性)
-
反射提供的功能:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
-
反射的优缺点:
- 优点:可以让咱们的代码更加灵活,为各种框架提供开箱即用的功能提供了便利
- 缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
-
框架可以理解为一个半成品的软件,利用框架进行开发可以大大简化我们的代码,例如我需要编写一款软件大概需要书写1w行代码,但是如果我利用框架进行开发,我大概只需要写1k行左右就可以了,其他的大概9k行都给你在框架中写好了,你配置去用就行了怎么样?听起来是不是很神奇?其精粹在于框架的设计过程中大量使用了Java的反射机制,将类的各个组成部分封装为其它对象,使得无论你定义了什么类,什么方法,我都可以去使用,具体使用过程我给你搞好了,你只需要写你要用的方法功能以及变量,修改一下配置文件就可以运行了,因此反射是框架设计的灵魂所在,同时框架也大大提高了我们的开发效率。https://blog.csdn.net/weixin_45453739/article/details/107024788
-
- 何为反射?
-
在java中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable类中有两个重要的子类Exception(异常)和Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。
- Exception:程序本身可以处理的异常,可以通过catch来进行捕获。Exception又可以分为受检查异常(必须处理)和不受检查异常(可以不处理)
- Error: Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止
- Exception又可以分为Checked异常和Runtime异常(运行时异常),所有的RuntimeException类及其子类的实例被称为Runtime异常,其他Exception类及其子类都属于受检查异常。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException…。
- throws声明抛出只能在方法签名中使用。一旦使用throws语句声明抛出该异常,程序就无须使用try…catch块来捕获异常了
- 如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
-
序列化:序列化机制允许奖实现序列化的java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。
-
对象的序列化指将一个Java对象写入到IO流中,与此对应的是,对象的反序列化则指从IO流中恢复该java对象
-
BigDecimal的用处
- 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。
- BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。
- BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念
-
Arrays.asList():可以将一个数组转换为List集合。
- 使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常
- Arrays.asList()的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList()体现的是适配器模式,只是转换接口,后台的数据仍是数组。
- 传递的数组必须是对象数组,而不是基本类型。
-
不要在foreach循环里进行元素的remove/add操作
-
- fail-fast:直接在容器上进行遍历,在遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常导致遍历失败。java.util包下的集合类都是快速失败机制的, 常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList
- fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。**原理:**采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
-
局部内部类,匿名内部类访问的局部变量必须使用final修饰,从Java8开始这个限制被取消了,Java8更加智能,如果局部变量被匿名内部类访问,相当于该局部变量自动使用了final修饰
-
final关键字:意思是最终的,不可修改的,最见不得变化,用来修饰类,方法和变量
- 1,final修饰的类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法
- 2,final修饰的方法不能被重写
- 3,final修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则对其初始化之后便不能让其指向另一个对象。
- 说明:使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
-
static关键字主要有以下四种使用场景:
- 修饰成员变量和方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,jdk1.8之后静态变量 存放在 Java 内存区域的堆中。调用格式:类名.静态变量名 类名.静态方法名()
- 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
- 静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。
- 静态导包(用来导入类中的静态资源,1.5 之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法
-
super关键字用于从子类访问父类的变量和方法
-
使用 this 和 super 要注意的问题:
- 在构造器中使用super()调用父类中的其他构造方法时,该语句必须处于构造器的首行。否则编译器会报错。另外,this调用本类中的其他构造方法时,也要放在首行。
- this,super不能用在static方法中(解释:被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this 和 super 是属于对象范畴的东西,而静态方法是属于类范畴的东西。)
-
static{}静态代码块与{}非静态代码块(构造代码块)
- 相同点: 都是在 JVM 加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些 static 变量进行赋值。
- 不同点: 静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次 new 执行一次,之后不再执行,而非静态代码块在每 new 一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
- 修正:静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过 Class.forName(“ClassDemo”)创建 Class 对象的时候也会执行,即 new 或者 Class.forName(“ClassDemo”) 都会执行静态代码块。
- 非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容
-
如何理解
-
获取Class对象的四种方式
- 1,知道具体类的情况下可以使用
- Class alunbarClass = TargetObject.class;
- 但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
- 2,通过 Class.forName()传入类的路径获取:
- Class alunbarClass1 = Class.forName(“cn.javaguide.TargetObject”);
- 3,通过对象实例instance.getClass()获取
- TargetObject o = new TargetObject();
- Class alunbarClass2 = o.getClass();
- 4,通过类加载器xxxClassLoader.loadClass()传入类路径获取:
- Class clazz = ClassLoader.loadClass(“cn.javaguide.TargetObject”);
- 通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行
- 1,知道具体类的情况下可以使用
-
代理模式
- 代理模式是一种比较好理解的模式,简单来说就是我们使用代理对象来代替真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
- 代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后可以增加一些自定义的操作
-
静态代理:静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
- 静态代理实现步骤
- 1,定义一个接口和实现类
- 2,创建一个代理类同样实现这个接口
- 3,将目标对象注入代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
- 静态代理实现步骤
-
动态代理:相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类(CGLIB)动态代理机制
- 从JVM角度来说,动态代理是在运行时动态生成类字节码,并加载到JVM中的。
-
JDK动态代理:
- 在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
- Proxy类中使用频率最高的方法是newProxyInstance(),这个方法主要用来生成一个代理对象
public static Object newProxyInstance(ClassLoader loader, //类加载器,用于加载代理对象
Class<?>[] interfaces, //被代理类实现的一些接口
InvocationHandler h) //实现了InvocationHandler对象的接口
throws IllegalArgumentException
{
......
}
- 要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法的时候,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
* invoke() 方法有下面三个参数:
* proxy :动态生成的代理类
* method : 与代理类对象调用的方法相对应
* args : 当前 method 方法的参数
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
- JDK动态代理类使用步骤:
- 1,定义一个接口及其实现类
- 2,自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑
- 3, 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
- CGLIB动态代理机制
- JDK动态代理有一个最致命的问题是其只能代理实现了接口的类,为了为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
- CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
- 在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心
public interface MethodInterceptor
extends Callback{
// 拦截被代理类中的方法
/**
* obj :被代理的对象(需要增强的对象)
* method :被拦截的方法(需要增强的方法)
* args :方法入参
* methodProxy :用于调用原始方法
*/
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
-
JDK 动态代理和 CGLIB 动态代理对比
- 1,JDK动态代理只能代理实现了接口的类或者直接代理接口,而CGLIB可以代理未实现任何接口的类。另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 2,就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显
-
静态代理和动态代理的对比
- 1,灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口中一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的
- 2,JVM层面:静态代理在编译时就将接口,实现类,代理类这些都变成了一个个实际的class文件。而动态代理是在运行时动态生成类字节码,并加载到JVM中的。
-
I/O模型
- UNIX系统下,IO模型一共有5种:同步阻塞I/O,同步非阻塞I/O,I/O多路复用,信号驱动I/O和异步I/O
- 同步阻塞I/O:应用程序发起read操作后,会一直阻塞,直到内核把数据拷贝到用户空间
- 同步非阻塞I/O:应用程序发起调用后,不会阻塞,而是会立即返回,然后不断进行轮询操作,询问数据是否准备好了,非阻塞IO模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),依然是阻塞的。因此它还是同步IO。
- 这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的
-
I/O多路复用模型:线程首先发起select调用,询问内核数据是否准备就绪,等内核数据准备好了,用户线程再发起read调用。read调用的过程(数据从内核空间–>用户空间还是阻塞的)还是阻塞。
- IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗
-
AIO
-
异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
面经
- String为什么要设计成不可变的?
- 1,便于实现字符串池
- 在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
- 2,使多线程安全
- 在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
- 3,避免安全问题
- 在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
- 1,便于实现字符串池
- 4,加快字符串处理速度
- 由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
第二章 java集合
-
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
-
Comparable和Comparator区别
- comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
- comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序
-
为什么要用ArrayList代替Vector?
- 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
- 性能:ArrayList 在性能方面要优于 Vector。
- 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%
-
无序性和不可重复性的含义是什么?
- 1,无序性:无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
- 2,不可重复性:不可重复性是指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashcode()方法
-
HashMap和Hashtable的区别
- 线程是否安全
- 底层数据结构
- 对Null key和Null value的支持:HashMap可以存储null的key和value,但null作为键只能有一个,作为值可以有多个;HashTable不允许有null键和null值,否则会抛出NullPointerException。
- 初始容量大小和每次扩容容量大小的不同:① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方
- 效率
-
HashMap 和 TreeMap 区别
-
TreeMap和HashMap都继承自AbstractMap,但是需要注意的是TreeMap还实现了NavigableMap接口和SortedMap接口。
-
实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
-
-
实现SortMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。
-
-
HashMap的底层实现
- JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
- 所谓的扰动函数指的就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,换句话说使用扰动函数之后可以减少碰撞。
- HashMap的长度为什么是2的幂次方
- 为了能让HashMap存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀。Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
- **这个算法应该如何设计呢?**我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方
-
遍历集合元素的方式
- 1,迭代器(Iterator)EntrySet的方式进行遍历
- 2,For Each方式遍历
- 3,Lambda表达式遍历。Java为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable接口是Collection接口的父接口,因此Collection集合也可直接调用该方法。(JDK1.8+)
- 4,Streams API遍历 (JDK1.8+)
-
Collections工具类常用方法
-
1,排序
-
2,查找,替换操作
-
3,同步控制(不推荐,需要线程安全的集合类型时请考虑使用JUC包下的并发集合)
集合源码分析
1,ArrayList
-
ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
-
ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口
- RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
- ArrayList 实现了 java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
-
以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10
-
ArrayList扩容机制
- 1,add方法
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
//添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
- 2,ensureCapacityInternal()方法
当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
- 3,ensureExplicitCapacity() 方法。如果调用 ensureCapacityInternal() 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
-
分析:
- 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。
- 当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。
- 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。
- 直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。
-
4,grow() 方法
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 5,hugeCapacity() 方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
System.arraycopy()和Arrays.copyOf()方法
- System.arraycopy() 方法
// 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
/**
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
- Arrays.copyOf()方法
public static int[] copyOf(int[] original, int newLength) {
// 申请一个新的数组
int[] copy = new int[newLength];
// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
-
两者的联系与区别:
- 联系:看两者源代码可以发现copyOf()内部实际调用了System.arraycopy()方法
- 区别: arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组
-
ensureCapacity方法
/**
如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
*
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
- 最好在add大量元素之前用ensureCapacity方法,以减少增量重新分配的次数
2, LinkedList
- 在根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。
Node<E> node(int index) {
// 判断插入的位置在链表前半段或者是后半段
if (index < (size >> 1)) { // 插入位置在前半段
Node<E> x = first;
for (int i = 0; i < index; i++) // 从头结点开始正向遍历
x = x.next;
return x; // 返回该结点
} else { // 插入位置在后半段
Node<E> x = last;
for (int i = size - 1; i > index; i--) // 从尾结点开始反向遍历
x = x.prev;
return x; // 返回该结点
}
}
3, HashMap
-
loadFactor 加载因子
- loadFactor是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是越趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏
- loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
-
put方法分析
/**
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
* 返回先前key对应的value值(如果value为null,也返回null),如果先前不存在这个key,那么返回的就是null;
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/
* 在往haspmap中插入一个元素的时候,由元素的hashcode经过一个扰动函数之后再与table的长度进行与运算才找到插入位置,下面的这个hash()方法就是所谓的扰动函数
* 作用:让key的hashCode值的高16位参与运算,hash()方法返回的值的低十六位是有hashCode的高低16位共同的特征的
* 举例
* hashCode = 0b 0010 0101 1010 1100 0011 1111 0010 1110
*
* 0b 0010 0101 1010 1100 0011 1111 0010 1110 ^
* 0b 0000 0000 0000 0000 0010 0101 1010 1100
* 0b 0010 0101 1010 1100 0001 1010 1000 0010
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
jdk1.7的put方法:
- 如果定位到的数组位置没有元素 就直接插入
- 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素
-
HashMap中有如下三个方法
// Callbacks to allow LinkedHashMap post-actions
//三个方法表示的是在访问、插入、删除某个节点之后,进行一些处理,它们在LinkedHashMap都有各自的实现。LinkedHashMap正是通过重写这三个方法来保证链表的插入、删除的有序性。
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
4,ConcurrentHashMap1.7 源码分析
/**
* 在构造函数未指定初始大小时,默认使用的map大小
* 这里的容量指的是所有segments中的HashEntry中的数量
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 默认的扩容因子,当初始化构造器中未指定时使用。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 默认的并发度,这里所谓的并发度就是能同时操作ConcurrentHashMap(后文简称为chmap)的线程的最大数量,
* 由于chmap采用的存储是分段存储,即多个segement,加锁的单位为segment,所以一个cmap的并行度就是segments数组的长度,
* 故在构造函数里指定并发度时同时会影响到cmap的segments数组的长度,因为数组长度必须是大于并行度的最小的2的幂。
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 每个分段最小容量
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/**
* 分段最大的容量
*/
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
/**
* 默认自旋次数,超过这个次数直接加锁,防止在size方法中由于不停有线程在更新map
* 导致无限的进行自旋影响性能,当然这种会导致ConcurrentHashMap使用了这一规则的方法
* 如size、clear是弱一致性的。
*/
static final int RETRIES_BEFORE_LOCK = 2;
/**
* 用于索引segment的掩码值,key哈希码的高位用于选择segment
*/
final int segmentMask;
/**
* 用于索引segment偏移值
*/
final int segmentShift;
/**
* Segment数组
*/
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
4.1 构造方法
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
// 参数校验
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 校验并发级别大小,大于 1<<16,重置为 65536
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
// 2的多少次方
int sshift = 0;
int ssize = 1;
// 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值
// 因为最后求hash值是通过hashcode & ssize-1,所以是2的次方值
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 记录段偏移量
this.segmentShift = 32 - sshift;
// 记录段掩码
this.segmentMask = ssize - 1;
// 设置容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
//Segment 中的类似于 HashMap 的容量至少是2或者2的倍数
while (cap < c)
cap <<= 1;
// create segments and segments[0]
// 创建 Segment 数组,设置 segments[0]
//每个Segment数组中的HashEntry的数量也是为2的次方,因为还要再次进行hash
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
4.2 java中的Unsafe类
4.3 ConcurrentHashMap中的key与value为什么不能为null
4.4 Integer的numberOfLeadingZeros方法解释
4.5 put源码
- SSHIFT
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p> The value can be retrieved by calling the <tt>get</tt> method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算
// 其实也就是把高4位与segmentMask(1111)做与运算
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 如果查找到的 Segment 为空,初始化
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
/**
* Returns the segment for the given index, creating it and
* recording in segment table (via CAS) if not already present.
*
* @param k the index
* @return the segment
*/
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
// 判断 u 位置的 Segment 是否为null
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
// 获取0号 segment 里的 HashEntry<K,V> 初始化长度
int cap = proto.table.length;
// 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的
float lf = proto.loadFactor;
// 计算扩容阀值
int threshold = (int)(cap * lf);
// 创建一个 cap 容量的 HashEntry 数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
// 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 自旋检查 u 位置的 Segment 是否为null
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
// 使用CAS 赋值,只会成功一次
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
4.6 entryAt()方法
4.7 put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 计算要put的数据位置
int index = (tab.length - 1) & hash;
// CAS 获取 index 坐标的值
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
// 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// first 有值没说明 index 位置已经有值了,有冲突,链表头插法。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 容量大于扩容阀值,小于最大容量,进行扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
- ReentrantLock中的tryLock()和lock()有什么区别
- tryLock(),立马返回,true或者false,不会阻塞
- lock(),不能获取锁,就会阻塞
4.7 scanAndLockForPut()智能获取锁,而非lock()暴力获取
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);//this表示当前的segment对象
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 自旋获取锁
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
// 自旋达到指定次数后,阻塞等到只到获取到锁
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
4.8 entryForHash
5,ConcurrentHashMap1.8 源码分析
5.1 hash值说明
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
5.1 sizeCtl
- 以volatile修饰的sizeCtl用于数组初始化与扩容控制,它有以下几个值:
- -N 表示有N-1个线程正在进行扩容操作,这句话是错误的。
这里-N的定义是有问题的,应该取-N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容。
当前未初始化:
= 0 //未指定初始容量
> 0 //由指定的初始容量计算而来,再找最近的2的幂次方。
//比如传入6,计算公式为6+6/2+1=10,最近的2的幂次方为16,所以sizeCtl就为16。
初始化中:
= -1 //table正在初始化
= -N //N是int类型,分为两部分,高15位是指定容量标识,
初始化完成:
=table.length * 0.75 //扩容阈值调为table容量大小的0.75倍
5.2 initTable()
- initTable()用于里面table数组的初始化,值得一提的是table初始化是没有加锁的,那么如何处理并发呢?
由下面代码可以看到,当要初始化时会通过CAS操作将sizeCtl置为-1,而sizeCtl由volatile修饰,保证修改对后面线程可见。这之后如果再有线程执行到此方法时检测到sizeCtl为负数,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()让出一次CPU执行时间。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
//正在初始化时将sizeCtl设为-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //DEFAULT_CAPACITY为16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); //扩容阈值为新容量的0.75倍
}
} finally {
sizeCtl = sc; //扩容保护
}
break;
}
}
return tab;
}
5.3 tabAt()/casTabAt()/setTabAt()
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
- 要原子语义的写操作需要使用casTabAt(),setTabAt()是在锁定桶的状态下才会被调用,之所以实现成这样只是带保守性的一种写法而已
5.4 扩容分析
-
当在扩容时,可以put元素。
-
什么时候会扩容:
- 使用put()添加元素时会调用addCount(),内部检查sizeCtl看是否需要扩容
- tryPresize()被调用,此方法被调用有两个调用点
- 链表转红黑树(put()时检查)时如果table容量小于64(MIN_TREEIFY_CAPACITY),则会触发扩容
- 调用putAll()之类一次性加入大量元素,会触发扩容
5.4.1 addCount()
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//LongAdder
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
5.4.2 fullAddCount()方法
5.4.3 transfer()f方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//最小的任务单元是16个
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
// 步长
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 初始化 nextTab
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 设置transferIndex
transferIndex = n;
}
int nextn = nextTab.length;
// 转移标识节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// 计算和控制对table 表中位置i 的数据进行转移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// 本轮转移完毕
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 更新TRANSFERINDEX(stride 为步长)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//扫描table 本轮完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果finished,则设置新tabl,sizeCtl ,返回
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//利用CAS方法更新这个扩容阈值,sizectl值减一,即将退出扩容
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//有其它线程在辅助,直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//扩容完成,辅助的线程也退出了,设置完成标识,最后再recheck 一下
finishing = advance = true;
i = n; // recheck before commit
}
}
// table 索引i 的位置上没有数据
else if ((f = tabAt(tab, i)) == null)
// 设置 为ForwardingNode 节点,这样其它线程可以感知到table 状态
advance = casTabAt(tab, i, null, fwd);
//已经设置过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 位置上是数据,需要锁住该位置,进行数据转移
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 链表数据
if (fh >= 0) {
// 得到hash 值 高位的二进制值(高位:n的二进制位数)
int runBit = fh & n;
// 最后添加的数据
Node<K,V> lastRun = f;
// 遍历链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 得到hash 值 高位的二进制值
int b = p.hash & n;
//和前个节点不一致,则更新
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 更加runBit 设置相应值
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 遍历链表,注意调节p != lastRun
// lastRun 之后的数据其高位也是一致
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 高位是0,则索引没有变,添加到ln 链表中
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
// 索引变为i+n的数据添加到hn 链表中
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 设置nextTab 索引i 的位置上的数据为链表ln
setTabAt(nextTab, i, ln);
// 设置nextTab 索引i 的位置上的数据为链表hn
setTabAt(nextTab, i + n, hn);
//原table 索引i 的位置上的数据转移完成,用ForwardingNode节点标识
setTabAt(tab, i, fwd);
advance = true;
}
// 是红黑树
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//这里是链式遍历,而不是树的方式遍历
// 这和我们前面说的一致:树中保存了链式关系,在构造树的时候构建的。
for (Node<K,V> e = t.first; e != null; e = e.next) {
// 下面也是分成两部分,重新构建TreeNode 链表
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果不满足树要求,则转化为链表,否则转换为TreeBin 对象(内部建树)
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 设置nextTab 索引i 的位置上的数据为ln(可能是链表,也可能是树)
setTabAt(nextTab, i, ln);
// 设置nextTab 索引i+n 的位置上的数据为hn
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
// 重设标识,继续循环,查找下次需要转移数据的索引i
advance = true;
}
}
}
}
}
}
5.4.4 fullAddCount()
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 获取当前线程的probe值
// 如果为0,则初始化当前线程probe值
if ((h = ThreadLocalRandom.getProbe()) == 0) {
// 该静态方法会初始化当前线程所持有的随机值
ThreadLocalRandom.localInit(); // force initialization
// 获取生成后的probe值,用于选择cells数组下标元素
h = ThreadLocalRandom.getProbe();
// 由于重新生成了probe,未冲突标志位设置为true
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// cells数组已经被成功初始化
if ((as = counterCells) != null && (n = as.length) > 0) {
// 通过该值与当前线程probe求与,获得cells的下标元素,和hash 表获取索引是一样的
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy 为0表示cells数组不在初始化或者扩容状态下
if (cellsBusy == 0) { // Try to attach new Cell
//创建新cell
CounterCell r = new CounterCell(x); // Optimistic create
// CAS设置cellsBusy,防止其它线程来破坏数据结构
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//将新创建的cell放入对应下标位置
rs[j] = r;
created = true;
}
} finally {
//恢复标识位(未占用)
cellsBusy = 0;
}
//操作成功,则退出死循环
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 获取了probe对应cells数组中的下标元素,发现不为空
// 并且调用该函数前,调用方CAS操作也已经失败(已经发生竞争)
else if (!wasUncontended) // CAS already known to fail
// 设置未冲突标志位后,重新生成probe,进入死循环
wasUncontended = true; // Continue after rehash
//CAS 执行累加
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// 成功,退出
break;
// 如果数组比较大了,则不需要扩容,继续重试,或者已经扩容了,重试
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// 过期了,重试
else if (!collide)
collide = true;
// 对Cell数组进行扩容,CAS设置cellsBusy值
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
//容量增加1倍
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
// 恢复
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//重新生成一个probe值
h = ThreadLocalRandom.advanceProbe(h);
}
// 初始化Cell 数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
// 默认初始容量为2
CounterCell[] rs = new CounterCell[2];
//将x的值设置到cell 数组中
rs[h & 1] = new CounterCell(x);
counterCells = rs;
// 初始化成功
init = true;
}
} finally {
// 恢复
cellsBusy = 0;
}
// 初始化成功,退出死循环
if (init)
break;
}
//竞争激烈,其它线程占据cell 数组,直接累加在base变量中
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
5.5 get()方法
- 如果首节点是ForwardingNode,则调用的是ForwardingNode的find方法
//会发现源码中没有一处加了锁
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //计算hash
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
6,Collections工具类
6.1 排序
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
6.2 查找,替换操作
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素,只代替和旧值一样的。
6.3 同步控制
- Collections 提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
- 我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections 提供了多个静态方法可以把他们包装成线程同步的集合。
- 最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。