JVM:
JVM是Java Virtual Machine(Java虚拟机)的缩写,java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。JVM的主要工作是解释自己的指令集(即字节码)到CPU的指令集或对应的系统调用。
JRE:
JRE是java runtime environment*(java运行环境)的缩写。光有JVM还不能让class文件执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。在JDK的安装目录里你可以找到jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和lib和起来就称为jre。所以,在你写完java程序编译成.class之后,你可以把这个.class文件和jre一起打包发给朋友,这样你的朋友就可以运行你写程序了(jre里有运行.class的java.exe)。
JDK:
JDK是java development kit(java开发工具包)的缩写。其中,真正在运行java时起作用的是以下四个文件夹:bin、include、lib、jre。现在我们可以看出这样一个关系,JDK包含JRE,而JRE包含JVM。
总的来说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。eclipse、idea等其他IDE有自己的编译器而不是用JDK bin目录中自带的,所以在安装时你会发现他们只要求你选jre路径就ok了。
JDK,JRE,JVM 三者关系概括如下:
jdk是JAVA程序开发时用的开发工具包,其内部也有JRE运行环境JRE。JRE是JAVA程序运行时需要的运行环境。JDk、JRE内部都包含JAVA虚拟机JVM,JAVA虚拟机内部包含许多应用程序的类的解释器和类加载器等等
引用数据类型:
引用类型指向一个对象,不是原始值,指向对象的变量是引用变量
在java里面除去基本数据类型的其他类型都是引用类型,自己定义的class类都是引用类型,可以像基本类型一样使用。
引用类型常见的有:String、StringBuffer、ArrayList、HashSet、HashMap等。
equals()方法的默认行为是比较引用,如果是自己写的类,应该重写equals()方法来比较对象的内容,大多数java类库都实现了比较对象内容的equals()方法。
类型不同:基本类型有八种,引用类型有四种
每个数组元素有默认值 double 0.0; boolean false; int 0
数组中可以存储基本数据类型,可以存储引用数据类型;但是对于一个数组而言,数组的类型是固定的,只能是一个
length:数组的长度
**数组的长度是固定的,一经定义,不能再发生变化(数组的扩容 **
2 << 3
https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序 和 外排序
内排序:将需要处理的数据都加载到内部存储器中进行排序
外排序:需要借助外部存储进行排序(先加载一部分,再加载一部分)
时间复杂度:
插入,冒泡排序,选择排序
快速排序,堆排序,归并排序
基数排序,桶排序 时间复杂度 O(n)
快排空间复杂度 为log2n,这里指的是随机快速排序。
冒泡拍戏和选择排序没人用了
归并排序思想:理解分治
快排:
堆排序:
稳定性的意义:
桶排序,计数排序,基数排序不是基于比较的排序,和样本的实际数据情况有很大关系,所以实际中并不经常使用
桶排序的应用场景十分严苛,首先,数据应该分布比较均匀。讲一种较坏的情况,如果数据全部都被分到一个桶里,那么桶排序的时间复杂度是不是就退化到O(nlogn)了呢?其次,要排序的数据应该很容易分成m个桶,每个桶也应该有大小顺序。
每次选择一个数字插入到有序的数组里面
这种方式可以跟插入排序一起学习
有两种实现方式:一种是交换(效率很低下)
一种是插入(效率会高很多)
每次选择一个最小的值交换到未排部分的 最开头
基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。
我感觉跟桶排序没什么区别
面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做
面向对象:将功能封装进去对象,强调的具备了功能的对象,以类/对象为最小单位,考虑谁来做
属性:对应类中的成员变量(field)
行为:对应类中的成员方法(method)
1、封装:就是把对象的属性和操作(或服务)结合为一个独立的整体,将将不需要对外提供的内容都隐藏起来、提供公共方法对其访问。好处 :提高安全性
2、继承: 继承就是子类继承父类的特征和行为,子类可以拥有父类的属性和方法。 子类可以拥有自己的属性和方法。而且还可以重写覆盖父类的方法
好处:提高代码复用性,便于扩展
3、多态:父类的引用指向子类的对象 当调用父类同名同参数的方法时候,实际执行的是子类重写父类的方法 – 虚拟方法调用
多态的使用:有了对象的多态性以后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法(方法是编译看左边,运行看右边),对于属性(变量)是编译运行都是看左边
优点:
多态的使用前提:1,有类的继承关系; 2.子类要重写父类的方法; 3. 父类引用指向子类对象
虚拟方法调用:简单来说就是编译看左边,运行时看右边。编译的时候,认为调用的是父类的方法,但是运行的时候,实际执行的是子类重写父类的方法。
问题:子类能否获取父类中Private权限的属性或方法?获取不是调用,封装解决的是调用的问题,继承解决的是获取的问题
可以的,私有属性提供get,set方法,私有方法,放在公共方法里面,也可以获取。
多态好处:便于拓展(继承保证),解耦,适应不断变化的需求。
//说明:eat,walk是子类重写父类的方法,earnMoney是子类特有的方法,
Person p2 = new Man();
p2.eat();
p2.walk();
//p2.earnMoney();这里会报错 不能调用子类特有的方法
编译看左边,运行看右边。
编译的时候会认为p2是person类型
在这几行里面,最后一行会报错,上面不会报错,点进去,到的是父类的方法,但是最后执行的是子类的方法
对象的多态性适用于方法,不适用属性;//比如父类id = 1001,子类属性id=1002,打印p2.id结果是1001
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时候,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
那如何调用子类特有的属性和方法?
Man m1 = (Man) p2;//向下转型
但是向下转型可能会失败;
Woman w1 = (Woman)p2;
w1.goShopping();
是运行时行为
重载是编译时多态,覆盖表现出两种多态性;编译时多态,运行时多态
补充下定义细节:
1、定义细节
重载(Overload)是让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者类型的同名函数(返回值类型可随意,不能以返回类型作为重载函数的区分标准)同时存在于同一个类中,是一个类中多态性的一种表现(调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性)。
重写(Override)是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写,不过子类函数的访问修饰权限不能小于父类的;若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用 super 关键字。
**重载规则:**必须具有不同的参数列表; 可以有不同的返回类型;可以有不同的访问修饰符;可以抛出不同的异常。
重写规则:参数列表必须完全与被重写的方法相同,否则不能称其为重写;返回类型必须一直与被重写的方法相同,否则不能称其为重写;访问修饰符的限制一定要大于等于被重写方法的访问修饰符;
重载与重写是 Java 多态性的不同表现。
重写是父类与子类之间多态性的表现,在运行时起作用(动态多态性,譬如实现动态绑定)
而重载是一个类中多态性的表现,在编译时起作用(静态多态性,譬如实现静态绑定)。
注意:static可以修饰属性,也就是类里面的,但是不可以在方法里面修饰变量
static可以修饰属性、方法、代码块和内部类;注意static不可以修饰构造器
static属性属于这个类所有,即由该类创建的所有对象共享同一个static属性。可以对象创建后通过对象名.属性名和
类名.属性名两种方式来访问。也可以在没有创建任何对象之前通过类名.属性名的方式来访问。
static属性和非static属性的区别(都是成员变量,不是局部变量)
static修饰方法,就是静态方法:随着类的加载而加载,可以直接通过类调用(想想以前做力扣,为什么非要加个static才可以直接用)
静态方法中,只能调用静态的方法或属性,非静态方法中,即可以调用非静态的方法或属性,也可以调用静态的方法或属性。因为静态结构跟着类的生命周期结束,非静态是跟对象是同步;所以晚出生可以调用早出生的,早出生的不能调用晚出生的。
1.在内存中份数不同
不管有多少个对象,static变量只有1份。对于每个对象,实例变量都会有单独的一份
static变量是属于整个类的,也称为类变量。而非静态变量是属于对象的,也称为实例变量
当通过某一个对象修改静态变量的时候,会导致其他对象调用这个静态变量的时候,是修改过的。
3.访问的方式不同
实例变量: 对象名*.*变量名 *stu1.name=“小明明”;
静态变量:对象名.变量名 stu1.schoolName=“西二旗小学”; 不推荐如此使用
类名.*变量名 Student.schoolName=“东三旗小学”; 推荐使用
4.在内存中分配空间的时间不同
实例变量:创建对象的时候才分配了空间。静态变量:随着类的加载而加载,早于对象的创建,由于类只会加载一次,则静态变量在内存当中也只会存在一份。
Student.schoolName=“东三旗小学”;或者Student stu1 = new Student(“小明”,“男”,20,98);
static方法也可以通过对象名.方法名和类名.方法名两种方式来访问
static代码块。当类被第一次使用时(可能是调用static属性和方法,或者创建其对象)执行静态代码块,且只被执行一次,主要作用是实现static属性的初始化。
静态代码块VS非静态代码块:
静态代码块:内部可以有输出语句, 随着类的加载而执行,而且只会执行一次
非静态代码块:内部可以有输出语句,随着对象的创建而执行,每创建一个对象,就执行一次非静态代码块;作用:
static内部类:属于整个外部类,而不是属于外部类的每个对象。不能访问外部类的非静态成员(变量或者方法),
可以访问外部类的静态成员
static注意点:在静态的方法内,不能使用this关键字、super关键字。
5.开发中如何确定一个属性是否要声明为static的?
属性可以被多个对象共享的,不会随着对象的不同而不同的。
如何确定一个方法是否要声明一个方法是static的?
操作静态属性的方法,通常设置为static的
工具类中的方法,习惯上声明为stactic的 因为没有必要造对象
饿汉式:对象加载时间过长。
饿汉式线程安全
懒汉式:延迟对象创建
final可以用来修饰的结构,类,方法,变量
final:用来修饰一个类:此类不能被其他类继承。比如String,System,StringBuilder。
final:用来修饰方法:表明这个方法不能被重写 比如object的getClass()
final:用来修饰变量,此时的变量就成为一个常量
abstract可以修饰类,方法
修饰类,则这个类不可以被实例化。抽象类中一定有构造器,便于子类实例化调用 抽象类可以定义构造器
开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作。
abstract修饰方法:抽象方法
抽象方法只有方法的声明,没有方法体,包含抽象方法的类一定是抽象类。反之,抽象类中可以没有抽象方法。
若子类重写父类中所有抽象方法后,此类才可以实例化。
若子类没有重写父类中所有抽象方法,则子类也是一个抽象类,需要使用abstract修饰。
注意:abstract不能用来修饰:属性、构造器等结构
不能用来修饰私有方法
final和abstract是功能相反的两个关键字,可以对比记忆
abstract可以用来修饰类和方法,不能用来修饰属性和构造方法;使用abstract修饰的类是抽象类,需要被继承,使用abstract修饰的方法是抽象方法,需要子类被重写。
final可以用来修饰类、方法和属性,不能修饰构造方法。使用final修饰的类不能被继承,使用final修饰的方法不能被重写,使用final修饰的变量的值不能被修改,所以就成了常量 。
注意:final修饰基本类型变量,其值不能改变,由原来的变量变为常量;但是final修饰引用类型变量,栈内存中的引用不能改变,但是所指向的堆内存中的对象的属性值仍旧可以改变。
类的访问权限只有两种:
public 公共的 可被同一项目中所有的类访问。 (必须与文件名同名)
default 默认的 可被同一个包中的类访问。
成员(成员变量或成员方法)访问权限共有四种:
public 公共的 可以被项目中所有的类访问。(项目可见性)
default 默认的被这个类本身访问;被同一个包中的类访问。(包可见性)
protected 受保护的 可以被这个类本身访问;同一个包中的所有其他的类访问;被它的子类(同一个包以及不同包
中的子类)访问。(子类可见性)
private 私有的 只能被这个类本身访问。(类可见性
注意:equals
1)对于==,比较的是值是否相等
如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址
2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量,equals继承Object类,比较的是是否是同一个对象
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
向上转型 通俗地讲即是将子类对象转为父类对象。此处父类对象可以是接口。
Animal b=new Bird(); //向上转型 --指的就是多态
向下转型:就是把父类对象转成子类对象
Animail a1=new Human();//向上转型
Human b1=(Human)a1;// 向下转型,编译和运行皆不会出错
这里的向下转型是安全的。因为 a1 指向的是子类对象。
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
Java中的基本数据类型却是不面向对象的(没有属性、方法),这在实际使用时存在很多的不便
很简单,下面两句代码就可以看到装箱和拆箱过程
1 //自动装箱
2 Integer total = 99;
3
4 //自动拆箱
5 int totalprim = total;
简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer。在JSP开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字符串,而int默认的默认值为0,所以用el表达式在文本框中显示时,结果为0,所以,int不适合作为web层的表单数据的类型。
从JVM的角度:Integer是存放在堆中,int是存放在栈中
接口在JDK1.7之前,只能定义全局常量和抽象方法。JDK8以后,还可以定义静态方法、默认方法
接口不能定义构造器,意味着接口不可以实例化。
如果实现类覆盖了接口中所有的抽象方法,则此实现类就可以实例化
如果实现类没有覆盖接口中的抽象方法,则此实现类仍为一个抽象类
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
1, 抽象类里可以有构造方法, 而接口内不能有构造方法。
2, 抽象类中可以有普通成员变量, 而接口中不能有普通成员变量。
3, 抽象类中可以包含非抽象的普通方法, 而接口中所有的方法必须是抽象的, 不能有非抽象的普通方法。
4, 抽象类中的抽象方法的访问类型可以是public , protected和private,但接口中的抽象方法只能是public类型的, 并且默认即为public abstract
类型。
5, 抽象类中可以包含静态方法, 接口内不能包含静态方法。
6, 抽象类和接口中都可以包含静态成员变量, 抽象类中的静态成员变量的访问类型可以任意, 但接口中定义的变量只能是public static类型, 并且
默认为public static final类型。
7, 一个类可以实现多个接口, 但只能继承一个抽象类。
接口可以继承接口
抽象类可以实现接口
抽象类可以继承实体类
final修饰基本类型变量,其值不能改变。
但是final修饰引用类型变量,栈内存中的引用不能改变,但是所指向的堆内存中的对象的属性值仍旧可以改变。例如
public class Test {
public static void main(String[] args) {
final Dog dog = new Dog("狗子");
dog.name = "泰迪";//正确
dog = new Dog("田园犬");//错误
}
}
Java创建对象的几种方式(重要): (1) 用new语句创建对象,这是最常见的创建对象的方法。 (2) 运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。 (3) 调用对象的clone()方法。 (4)运用反序列化手段,调用java.io.ObjectInputStream对象的 readObject()方法。 (1)和(2)都会明确的显式的调用构造函数 ;(3)是在内存上对已有对象的影印,所以不会调用构造函数 ;(4)是从文件中还原类的对象,也不会调用构造函数。
Collection和Collections区别
java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。
java.util.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。
事实上Collections.sort方法底层就是调用的array.sort方法,
equals
时必须重写 hashCode
方法?1.使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率
2.保证是同一个对象,如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。
https://blog.csdn.net/kiss_the_sun/article/details/7848920
https://blog.csdn.net/qq_37113604/article/details/81168224
https://blog.csdn.net/qq_37113604/article/details/81168224
浅拷贝:
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
(2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。
深拷贝:
https://blog.csdn.net/qq_42022528/article/details/80537120
1、 sleep 来自 Thread 类,和 wait 来自 Object 类。 2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使用范围)
4、 sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
对于 Java 数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目:
静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4
String[] computers = {"Dell", "Lenovo", "Apple", "Acer"}; //①
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
String[] names = new String[]{"多啦A梦", "大雄", "静香"}; //②
动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如:
//只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null
String[] cars = new String[4]; //③
因为 Java 数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所示:
由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 null ,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。
主要包括error和exception,exception包括运行时异常和编译异常
运行时异常:nullpointerException,indexoutofBoundsException,不用捕获
编译异常,必须捕获处理,OException、SQLException等以及用户自定义的Exception异常
/**
* 1. 异常最上面的类是throwable
* 2. throwalbe分为error和exception
* 3. error为错误异常(一般为虚拟机异常,非代码异常),exception(代码块会发生的异常)分为RuntimeException (运行性异常)与IO异常
* 4. 异常也可以分为可查的异常(编译器要求必须去处理的异常,进行try..catch, 或者throws抛出)与不可查的异常(可处理或不去处理去处理的异常)
* 4.1 不可查异常: runtimeException和他的子类以及错误(如ArithmeticException、RedisException), 可查异常: 除了不可查异常(ClassNotFoundException)。
* 5. 异常一般先处理小异常,再去处理大异常
* 6. 5个关键字, try...catch...finally...throw...throws.....
*
* 注意: 运行性异常下有很多异常,但只有几个是其子类,其他都属于exception子类哦。
**/
二、异常处理机制
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
抛出异常:
捕捉异常:
抛出异常的方法:throws和throw
throws:通常被用在声明方法时,用来指定方法可能抛出的异常,多个异常可使用逗号分隔。throws关键字将异常抛给上一级,如果不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的代码。
throw:通常用在方法体中或者用来抛出用户自定义异常,并且抛出一个异常对象。程序在执行到throw语句时立即停止,如果要捕捉throw抛出的异常,则必须使用try-catch语句块或者try-catch-finally语句。
1 序列化和反序列化的概念
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
2,什么情况下需要序列化
当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
当你想用套接字在网络上传送对象的时候;
当你想通过RMI传输对象的时候;
(老实说,上面的几种,我可能就用过个存数据库的)
3,如何实现序列化
实现Serializable接口即可
上面这些理论都比较简单,下面实际代码看看这个序列化到底能干啥,以及会产生的bug问题。
所以,有这么个理论,就是在实现这个Serializable 接口的时候,一定要给这个 serialVersionUID 赋值,就是这么个问题。
这也就解释了,我们刚刚开始编码的时候,实现了这个接口之后,为啥eclipse编辑器要黄色警告,需要添加个这个ID的值。而且还是一长串你都不知道怎么来的数字。
下面解释这个 serialVersionUID 的值到底怎么设置才OK。
首先,你可以不用自己去赋值,Java会给你赋值,但是,这个就会出现上面的bug,很不安全,所以,还得自己手动的来。
那么,我该怎么赋值,eclipse可能会自动给你赋值个一长串数字。这个是没必要的。
可以简单的赋值个 1L,这就可以啦。。这样可以确保代码一致时反序列化成功。
不同的serialVersionUID的值,会影响到反序列化,也就是数据的读取,你写1L,注意L大些。计算机是不区分大小写的,但是,作为观众的我们,是要区分1和L的l,所以说,这个值,闲的没事不要乱动,不然一个版本升级,旧数据就不兼容了,你还不知道问题在哪。。。
注意事项:
首先,comparable只有一个compareTo方法,comparator有compare等很多方法,
Comparable接口位于 java.lang包下,Comparator接口位于java.util包下。
comparable是需要比较的对象来实现接口。这样对象调用实现的方法来比较。对象的耦合度高(需要改变对象的内部结构,破坏性大)。
comparator相当于一通用的比较工具类接口。需要定制一个比较类去实现它,重写里面的compare方法,方法的参数即是需要比较的对象。对象不用做任何改变。解耦
Collections对于Integer类型的数组默认的排序结果是升序的
那么如果我们创建一个自定义类型的Person数组能否进行排序呢,大家可以用代码试一下,结果是不可以的,为什么会有这样的问题呢,我们去看一下Collections中的sort方法,就可以发现问题:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
在泛型的规则中,有一个T extends Comparable的泛型通配符 ,对于要排序的list中的T进行了一个限制,要求集合中的T必须要实现Comparable接口,我们可以按照这个思路,写一个Person类,实现Comparable接口,而这个接口中,有一个抽象方法需要我们实现,这个方法就是CompareTo
public class Person implements Comparable<Person>{
String name;
Integer age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
//排序的规则
public int compareTo(Person o) {
//引用类型(可以排序的类型)可以直接调用CompareTo方法
//基本类型--使用 减
//return this.age - o.age;//用this对象 - 参数中的对象,是按照该属性的升序进行的排列
//return o.age - this.age; //降序
//return this.name.compareTo(o.name);
//return o.name.compareTo(this.name);
return this.age.compareTo(o.age);
}
二、Comparator
正如上文所说,对于已经实现了Comparable接口的集合,或者是我压根就不想实现Comparable接口的集合难道就排不了序了么,或者就无法更改排序的规则了么,实际上不是的,我们可以通过另一种方式来排序,就是利用Comparator接口。
在集合的工具类中种还有这样的一个方法:public static void sort(List list, Comparator super T> c)
我们可以通过这个方法实现上面的需求:
Collections.sort(list,new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
比如这段代码,就实现了一个Integer集合的降序排列。这个接口中有一个方法叫做compare,里边包含两个参数:如果用第一个和第二个做比较得到的就是升序,反之得到的就是降序。同样的你也可以使用这种方式对我们自己定义的类记性排序。
/**
* @desc AscAgeComparator比较器
* 它是“Person的age的升序比较器”
*/
//负数认为第一个数应该放前面 这个就是默认排序 即从小到大
private static class AscAgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
/**
* @desc DescAgeComparator比较器
* 它是“Person的age的降序比较器”
*/
private static class DescAgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p2.getAge() - p1.getAge();
}
}
vector可以制定扩充容量
ArrayList扩容机制:
// jdk1.7.0_79
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
vector扩容机制:是增长一倍
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
先搞清楚
是否有序指的是集合中的元素是否会按照插入的先后顺序来存储,而不是指集合中的元素本身是否有序
Connection包含List和Set两大分支:
List
public interface List<E> extends Collection<E>
List 中的元素都是有序的,其常见的实现类有 ArrayList、LinkedList 和 Vector,这三者都是有序的
Set
public interface Set<E> extends Collection<E>
Set 中的元素都是无序的,并且不能重复
它是根据对象的哈希值来确定元素在集合中的存储位置,所以是无序的;保证元素唯一性的方式依赖于 hashCode() 和 equals() 方法
其常见的实现类有 HashSet、LinkedHashSet,HashSet 的底层实现其实是由 HashMap 支持的,因此其存储结构其实也是哈希表,但要注意 Set 是单列集合,即不是以
注意:LinkedHashSet 是有序的
Map
public interface Map<K,V>
Map 中的元素都是无序的,其实的元素是以
注意:LinkedHashMap 是有序的
1、List( 有序、可重复)
ArrayList与Vector
①底层数据结构是数组,查询快,增删慢。
②前者效率高、线程不安全,后者效率低、线程安全。
LinkedList
①底层数据结构是链表,查询慢,增删快。
②线程不安全,效率高。
2、Set
HashSet(唯一、无序)
①底层数据结构是哈希表。
②两个方法:hashCode()和equals()
LinkedHashSet(唯一、有序)
①底层数据结构是链表和哈希表。
②由链表保证元素有序、哈希表保证元素唯一。
TreeSet(唯一、有序)
①底层数据结构是红黑树。
②支持自然排序、比较器排序。
③根据比较的返回值是否是0来决定是否唯一。
LinkedList类是List接口的实现类,说明它可以根据索引来随机访问集合中的元素,同时,LinkedList还实现了Deque接口,说明其可以被当作双端队列来使用,并且也可以当做栈来使用。
Collection继承了Iterable接口。List是链表 ,Queue 是队列, Set 是集合,注意集合中的元素是不能重复的。AbstractList是抽象队列,继承了List;Deque是双端队列,继承了Queue; SortedSet是排序集合,继承了Set。
第二张图(右):
Vector:底层由数组实现,查询快,增删慢。线程安全,效率底。线程安全是因为加了同步函数sychoronized来保证线程安全
LinkedList:底层由链表实现,查询慢,增删快。线程不安全,效率高。
Queue
Deque:
Deque当做队列使用时候,队列从队尾部插入元素
Deque当做栈方法的时候: 栈是从头部插入元素
ArrayList 顺序表存储,访问快,插入和删除慢。
ArrayDeque 双向顺序存储,集成栈和队列方法,访问快,插入和删除慢。
LinkedList 双向链式表,集成栈和队列方法,访问慢,插入和删除快。
linkedlist和arrayDeque都可以当做栈和队列使用
List和Set总结:
(1)、List,Set都是继承自Collection接口,Map则不是
(2)、List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
(3)、Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
1、根据Key得到hashcode,根据长度求余数。(key.hashcode % len = key.hashcode & (len - 1))
2、扩容,想一下,链表怎么又扩容的说法,所以仅仅是对数组的扩容;
同样一个元素,扩容后,一般不需要重新算hash,而index变化也只有两种情况,hash新增的哪一位是1还是0,如果是0索引不会变,因为&后还是0,如果是1,index就变成原索引+oldCapcity;
单独链表法,其他的方法:开放地址法,通过计算出来冲突的hash值再次进行运算,直到得到可用的地址。
https://my.oschina.net/muziH/blog/1596801
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment 实现了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和 HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。
ConcurrentHashMap
取消了 Segment
分段锁,采用 CAS 和 synchronized
来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized
只锁定当前链表或红黑二叉树的首节点, 这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
(1)先计算出对应key的hash值,根据hash值计算index值,就是数组的位置。
(2)然后会判断当前数组位置有没有元素,没有的话就把值插到当前位置,有的话就说明遇到了哈希碰撞
(3)遇到哈希碰撞后,就会看下当前链表是不是以红黑树的方式存储,是的话,就会遍历红黑树,看有没有相同key的元素,有就覆盖,没有就执行红黑树插入
(4)如果是普通链表,则按普通链表的方式遍历链表的元素,判断是不是同一个key,是的话就覆盖,不是的话就追加到后面去
(5)当链表上的元素达到8个的时候,如果不满足扩容条件,链表会转换成红黑树;如果满足扩容条件,则hashmap会进行扩容,把容量扩大到以前的两倍
https://blog.csdn.net/woshimaxiao1/article/details/83661464
转换红黑树条件是链表节点大于等于8且数组长度大于等于64
1.7 版本主要就是 数组 + 链表,那么如果有一个 hash 值总是会发生碰撞,那么由此对应的链表结构也会越来越长,这个时候如果再想要进行查询操作,就会非常耗时,所以该如何优化这一点就是 1.8 版本想要实现的
1.8 版本采用了 数组 + 链表 + 红黑树 的方式去实现,当链表的长度大于 8 时,就会将链表转为红黑树。
这个时候问题就来了,为什么会将链表转红黑树的值设定为 8 ?
因为链表的时间复杂度是 n/2 ,红黑树时间复杂度是 logn ,当 n 等于 8 的时候, log8 要比 8/2 小,这个时候红黑树的查找速度会更快一些
https://www.bilibili.com/video/BV1z54y1i73r?from=search&seid=11849113556968522092
jdk1.7为什么采用头插法?
至于为什么会采用头插法,据说是考虑到热点数据的原因,即最近插入的元素也很可能最近会被使用到。所以为了缩短链表查找元素的时间,所以每次都会将新插入的元素放到表头。
HashMap 的线程不安全主要体现在两个方面:扩容时导致的死循环 & 数据覆盖
扩容时导致的死循环,这个问题只会在 1.7 版本及以前出现,因为在 1.7 版本及以前,扩容时的实现,采用的是头插法,在多线程情况下,这样就会导致循环链表的问题。(这个时候是get出问题)
什么时候会触发扩容呢?如果存储的数据,大于 当前的 HashMap 长度( Capacity ) * 负载因子( LoadFactor ,默认为 0.75) 时,就会发生扩容。比如当前容量是 16 , 16 * 0.75 = 12 ,当存储第 13 个元素时,经过判断发现需要进行扩容,那么这个时候 HashMap 就会先进行扩容的操作
扩容也不是简简单单的将原来的容量扩大就完事儿了,扩容时,首先创建一个新的 Entry 空数组,长度是原数组的 2 倍,扩容完毕之后还会再进行 ReHash ,也就是将原 Entry 数组里面的数据,重新 hash 到新数组里面去
1.8 版本之后采用了尾插法,解决了这个问题
还有个问题, 1.8 版本是没有解决的,那就是数据覆盖问题
假设现在线程 A 和线程 B 同时进行 put 操作,特别巧的是这两条不同的数据 hash 值一样,并且这个位置数据为 null ,那么是不是应该让线程 A 和 B 都执行 put 操作。假设线程 A 在要进行插入数据时被挂起,然后线程 B 正常执行将数据插入了,然后线程 A 获得了 CPU 时间片,也开始进行数据插入操作,那么就将线程 B 的数据给覆盖掉了
因为 HashMap 对 put 操作没有进行加锁的操作,那么就不能保证下一个线程 get 到的值,就一定是没有被修改过的值,所以 HashMap 是不安全的
hashmap:线程不安全,是基于hash表实现的
hashtable:线程安全,全局就一个锁,效率低下,相当于hashmap的线程安全版本
treemap:线程不安全, 实现的sortedmap接口,是基于红黑树的实现,适用于按自然顺序或自定义顺序遍历键(key)。
ConCurrentHashMap:分段锁,锁的粒度更细。
hashmap比treemap效率高,需要排序的时候可以用treemap
https://blog.csdn.net/justloveyou_/article/details/72783008
https://segmentfault.com/a/1190000039087868
segment长度第一次就确定了,后面扩容不会改变segment长度。
每个segment里面至少有2个hashEntry,扩容的时候,是segment扩容,不是整个扩容,也就是说segment大小可能只是刚开始初始化的时候每个segment相同,只有可能就不同了。
concurrentHashMap里面有一个UNsave,这个类只能被bootstrapclassloader加载器加载的类才可以使用,自定义的类使用会报错。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
JDK1.6
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
JDK1.7
//segment结构
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
}
//hashEntry结构
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
JDK1.8 中segment结构也有,但是里面东西很少
//变为Node结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(单线程情形下)。
下面是JDK1.7的介绍:
在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map
ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
总的来说,ConcurrentHashMap的高效并发机制是通过以下三方面来保证的(具体细节见后文阐述):
通过锁分段技术保证并发环境下的写操作;
通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;
通过不加锁和加锁两种方案控制跨段操作的的安全性。
HashMap结构:
Segment是什么呢?Segment本身就相当于一个HashMap对象。同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。
ConcurrentHashMap结构:
ConcurrentHashMap类中包含两个静态内部类 HashEntry 和 Segment,其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。
注意,假设ConcurrentHashMap一共分为2n个段,每个段中有2m个桶,那么段的定位方式是将key的hash值的高n位与(2n-1)相与。在定位到某个段后,再将key的hash值的低m位与(2m-1)相与,定位到具体的桶位。
ConcurrentHashMap并发读写的几种情形:
Case1:不同Segment的并发写入 不同Segment的写入是可以并发执行的。
Case2:同一Segment的一写一读 可以并发执行的
Case3:同一Segment的并发写入因此对同一Segment的并发写入会被阻塞。
由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
JDK1.7的时候put操作的步骤:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.获取可重入锁。如果获取失败就说明有其他线程存在竞争 ,就利用自旋获取锁尝试获取,如果重试次数达到一定数量,就改为阻塞获取,保证可以获取成功。
4.再次通过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。
JDK1.8的时候操作:
1.计算hash值
2.判断是否需要初始化
3.定位到Node 拿到首节点,判断首节点:
如果是null,通过CAS方式添加
如果是f.hash = -1,说明正在扩容
如果都满足,synchronize锁住f节点,判断是链表还是红黑树,遍历插入
4.超过8的时候,变成红黑树
JDK1.7的时候get操作的步骤:
首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。
由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
JDK1.8的时候get操作的步骤:
我们先来说value 为什么不能为 null。因为 ConcurrentHashMap 是用于多线程的 ,如果ConcurrentHashMap.get(key)
得到了 null ,这就无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,就有了二义性。
而用于单线程状态的 HashMap 却可以用containsKey(key)
去判断到底是否包含了这个 null
Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count; // Segment中元素的数量,可见的
transient int modCount; //对count的大小造成影响的操作的次数(比如put或者remove操作)
transient int threshold; // 阈值,段中元素的数量超过这个值就会对Segment进行扩容
transient volatile HashEntry<K,V>[] table; // 链表数组
}
在Segment类中,count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组包含的 HashEntry 对象的个数,也就是 Segment 中包含的 HashEntry 对象的总数。特别需要注意的是,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是对 ConcurrentHashMap 并发性的考虑:因为这样当需要更新计数器时,不用锁定整个ConcurrentHashMap。事实上,每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变),都要更新count的值,此外,在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是,count是volatile的,这使得对count的任何更新对其它线程都是立即可见的。
modCount用于统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变.table是一个典型的链表数组,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的。段(Segment)的定义如下:
HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash,next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点(这在谈ConcurrentHashMap的删除操作时还会详述)。特别地,由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因。实际上,ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。HashEntry代表hash链中的一个节点,其结构如下所示:
/**
* ConcurrentHashMap 中的 HashEntry 类
*/
static final class HashEntry<K,V> {
final K key; // 声明 key 为 final 的
final int hash; // 声明 hash 值为 final 的
volatile V value; // 声明 value 被volatile所修饰
volatile HashEntry<K,V> next; // 声明 next 为 final 的
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
与HashMap类似,在ConcurrentHashMap中,如果在散列时发生碰撞,也会将碰撞的 HashEntry 对象链成一个链表。由于HashEntry的next域是final的,所以新节点只能在链表的表头处插入。下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图(由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反):
hashEntry中的entry结构1.6
/**
* HashMap 中的 Entry 类
*/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
...
}
在ConcurrentHashMap中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作(比如,put操作、remove操作等)才需要加锁。
1、用分段锁机制实现多个线程间的并发写操作: put(key, vlaue)
ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。 此外,我们还可以看到,实际上我们对ConcurrentHashMap的put操作被ConcurrentHashMap委托给特定的段来实现。
从源码中首先可以知道,ConcurrentHashMap对Segment的put操作是加锁完成的。在第二节我们已经知道,Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。需要注意的是,这里的加锁操作是针对某个具体的Segment,锁定的也是该Segment而不是整个ConcurrentHashMap。因为插入键/值对操作只是在这个Segment包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。因此,其他写线程对另外15个Segment的加锁并不会因为当前线程对这个Segment的加锁而阻塞。故而 相比较于 HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。
HashEntry对象几乎是不可变的(只能改变Value的值),因为HashEntry中的key、hash和next指针都是final的。这意味着,我们不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性。与此同时,由于HashEntry类的value字段被声明是Volatile的,因此Java的内存模型就可以保证:某个写线程对value字段的写入马上就可以被后续的某个读线程看到。此外,由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时,便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:
1、用HashEntry对象的不变性来降低读操作对加锁的需求;
非结构性修改操作只是更改某个HashEntry的value字段的值。由于对Volatile变量的写入操作将与随后对这个变量的读操作进行同步,所以当一个写线程修改了某个HashEntry的value字段后,Java内存模型能够保证读线程一定能读取到这个字段更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程看到。
对ConcurrentHashMap做结构性修改时,实质上是对某个桶指向的链表做结构性修改。如果能够确保在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表,那么读/写线程之间就可以安全并发访问这个ConcurrentHashMap。在ConcurrentHashMap中,结构性修改操作包括put操作、remove操作和clear操作,下面我们分别分析这三个操作:
clear操作只是把ConcurrentHashMap中所有的桶置空,每个桶之前引用的链表依然存在,只是桶不再引用这些链表而已,而链表本身的结构并没有发生任何修改。因此,正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。
关于put操作的细节我们在上文已经单独介绍过,我们知道put操作如果需要插入一个新节点到链表中时会在链表头部插入这个新节点,此时链表中的原有节点的链接并没有被修改。也就是说,插入新的健/值对到链表中的操作不会影响读线程正常遍历这个链表。
2、用Volatile变量协调读写线程间的内存可见性;
3、若读时发生指令重排序现象,则加锁重读;
size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上,在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
那么,ConcurrentHashMap是如何判断在统计的时候容器的段发生了结构性更新了呢?我们在前文中已经知道,Segment包含一个modCount成员变量,在会引起段发生结构性改变的所有操作(put操作、 remove操作和clean操作)里,都会将变量modCount进行加1,因此,JDK只需要在统计size前后比较modCount是否发生变化就可以得知容器的大小是否发生变化。
CAS+synchronized
保证线程安全,实现更加细粒度的锁,将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。在剖析ConcurrentHashMap的put操作时,我们就知道ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。但是,此处怎么会存在键值对存在且的Value值为null的情形呢?JDK官方给出的解释是,这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用。这时,JDK给出的解决之道就是加锁重读。
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
HashMap1.8
的结构一样, Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode. Hashtable和 JDK1.8 之前的
HashMap的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment
的概念,而是直接用 Node
数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS 来操作。(JDK1.6 以后 对 synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
,虽然在 JDK1.8 中还能看到 Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable
(同一把锁) :使用 synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
如果你看过 HashSet
源码的话就应该知道:HashSet
底层就是基于 HashMap
实现的。(HashSet
的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet
自己不得不实现之外,其他方法都是直接调用 HashMap
中的方法。
ArrayList 就是动态数组, 是 Array 的复杂版本, 动态的增加和减少元素.当更多的元素加入到 ArrayList 中时,其大小将会动态地增长。 它的元素可以通过 get/set 方法直接访问,因为 ArrayList 本质上是一个数组。 初始容量为 10。
1.插入元素的时候可能扩容,删除元素时不会缩小容量。
2.扩容增长为 Arraylist 增长原来的 0.5 倍
3.而 Arraylist 没有设置增长空间的方法。
4.线程不同步
Vector 和 ArrayList 类似, 区别在于 Vector 是同步类(synchronized).因此,开销就比ArrayList 要大。 初始容量为 10。 实现了随机访问接口, 可以随机访问。 Vector 是内部是以 动态数组的形式来存储数据的。 1.Vector 还可以设置增长的空间大小, 2. 及 Vector 增长原来的 1 倍 3.vector 线程同步
LinkedList 是一个双链表,在添加和删除元素时具有比 ArrayList 更好的性能.但在 get与 set 方面弱于 ArrayList.当然,这些对比都是指数据量很大或者操作很频繁的情况下的对比。 它还实现了 Queue 接口,该接口比 List 提供了更多的方法,包括 offer(),peek(),poll()等.ArrayList 和 LinkedList 的使用场景, 其中 add 方法的实现 ArrayList,LinkedList 的实现,以及插入, 查找, 删除的过程 。
一、list容器扩容
容器特性:可重复,有序。
1、arrayList:线程不安全,读取速度快。
默认初始容量:10
加载因子:1,即元素个数大于容器大小才扩容。
扩容增量:原容量的1.5倍。假设原来容量为10,经过一次扩容后为15.
2、vector:线程安全,读取速度慢。
默认初始容量:10
加载因子:1,即元素个数大于容器大小才扩容。
扩容增量:原容量的0.5倍。假设原来容量为10,经过一次扩容后为15.
二、set容器扩容
容器特性:不可重复,无序。
1、hashSet:线程不安全,读取速度快;底层为hasnMap实现。
默认初始容量:16
加载因子:0.75,即元素个数大于容器的0.75就执行扩容。
扩容增量:原容量的1倍。假设原来容量为16,经过一次扩容后为32。
三、map容器扩容
容器特性:一个双列集合,key,value键值对。
1、hashMap:无序,key值不可重复。
默认初始容量:16
加载因子:0.75,即元素个数大于容器的0.75就执行扩容。
扩容增量:原容量的1倍。假设原来容量为16,经过一次扩容后为32。
2、hashTable:线程安全,但是速度慢,不允许key和value为null。
默认初始容量:11
加载因子:0.75,即元素个数大于容器的0.75就执行扩容。
扩容增量:原容量的1倍再加1。假设原来容量为11,经过一次扩容后为23。
1.List:有序,元素可重复
ArrayList、LinkedList和Vector是三个主要的实现类。ArrayList是线程不安全的, Vector 是线程安全的,这两个类底层都是由数组实现的LinkedList是线程不安全的,底层是由链表实现的。
2.Set:元素不可重复
HashSet和TreeSet是两个主要的实现类。Set 只能通过游标来取值,并且值是不能重复的。
3.Map: 键值对集合
其中key列就是一个集合,key不能重复,但是value可以重复。HashMap、TreeMap和Hashtable是Map的三个主要的实现类。HashTable是线程安全的,不能存储 null 值;HashMap不是线程安全的,可以存储 null 值。
多线程条件下会报java.util.ConcurrentModificationException
使用new Vector<>();其加锁sync,实现线程安全
Collections.synchronizedList(new ArrayList<>());
写时复制,使用new CopyOnWriteArrayList<>();
CopyOnWrite容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素后,再将原容器的引用指向新的容器setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁;因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器里面。
hashset线程不安全,HashSet底层是HashMap
Collections.synchronizedSet(new HashSet<>());
写时复制,使用new CopyOnWriteArraySet<>();
底层还是CopyOnWriteArrayList
hashmap线程不安全,使用ConcurrentHashMap.