面向过程和面向对象
我个人觉得是这样:面向过程是程序员设定好了程序执行的步骤,计算机去一条一条执行。而面向对象是程序员先做一个对象出来,然后告诉对象做什么。
区别:
- 面向对象的效率低一点,但是代码维护容易,可读性高
- 面向过程的效率高一点,但是代码维护不易,可读性低
Java的语言特点
- 继承
- 封装
- 多态(存在的条件是继承,重写。子类重写了父类的方法
- 线程安全
- 面向对象
- 编译解释共存
JDK、JVM、JRE
JRE = 运行环境
JDK是一个开发环境+运行环境
JVM帮助Java实现跨平台
JAVA和C++的区别
- JAVA没有指针而C++有指针。(这样JAVA的运行效率低一些但是安全程度大一点)
- JAVA有垃圾回收机制,释放内存,而C++需要手动释放内存。
- JAVA是单继承,而C++是多继承。JAVA通过接口实现多继承
字符型常量和字符串常量的区别
- 一个表示一个字符,一个表示多个字符
- 占据内存大小不同
- 一个是值一个是地址
重载和重写的区别
- 重写发生在继承上面的,子类重写父类的方法形成多态
- 重载是在一个类中,同名方法,返回值相同,但是参数不同。返回值不同是不能构成重载,也就是核心在于参数的变化。
String StringBuffer StringBuilder
- String类型的变量是不可变的,它的本质在底层维护了一个用final修饰的char类型数组
- Java维护一个字符串常量池,String类型变量放在其中
- 如果一个String类型的变量执行一个拼接操作。如 str1 = str2 + str3,会占用三块内存,效率很低下。
- Stringbuilder是StringBuffer的线程不安全版本。他们的底层是没有维护一个final修饰的char类型数组的,所以他们的执行会效率高很多。
装箱和拆箱
装箱= int to Integer
拆箱= Integer to int
静态方法
静态方法属于类方法,在编译的时候就存在了,不是像对象一样的。所以静态方法是不能调用非静态变量的,因为那个时候成员变量可能还没有声明。静态方法不能主动去调用成员变量主动
在Java中定义空构造函数
防止子类没有写super()导致错误
接口和抽象类的区别
首先介绍一下抽象方法
首先抽象类中不一定要有抽象方法,但是抽象方法只存在于抽象类。逻辑是:因为一个不是抽象类的类如果要实例化,那它的方法一定都是具体的,不然你实例化的对象如何调用呢。但是抽象类方正也不具体化自己,所以也无所谓有没有抽象方法(但是你吃饱了撑的去定义一个没有抽象方法的抽象类?)抽象方法是没有主体的!
如果定义了一个抽象类,并且含有非抽象方法,那么这个方法可以在子类中被重写。
接口和抽象类的区别
- 抽象类抽象的是对象,而接口抽象的是方法,这个就是编程思想上的问题了
- 通过接口来实现多继承,而抽象类只能单继承
- 接口中的所有变量都是使用final static修饰的。不管是是否主动增加了。所以在接口中定义的变量记得赋予初始值。
- 接口的方法默认为public abstract的。所以接口中是不能有实体方法的哦。
成员变量和局部变量的局别
- 生命周期不同。成员变量的生命周期是随着对象而定的。而局部变量除了它所属的代码块就没了。
- 局部变量不能被static修饰
- 成员变量会自动被附上初始值,如果是对象就是Null,基础变脸都有属于它的初始值
- 存储的地方不同。成员变量是对象的一部分,存在在堆中,而局部变量是放在栈中的。
构造方法
- 特点
- 没有返回值
- 与类同名
- 生成对象的时候自动调用
- 调用子类构造方法之前会调用父类构造方法,帮助类完成初始化
- 子类的构造方法和父类的构造方法
- 在子类中一边使用super这个关键字来代表父类。如果想使用父类的构造方法用super()就可以了。一般类会默认有一个空的构造方法,如果没有使用super()关键字会自动调用这个空的构造方法。
== 和 equals的区别
- == 表示值的相等。如果是基本类型就是值的相等,如果是引用类型就是地址相同。
- equals其实是可以重写的,如果没有重写equals,它等价于==,如果重写了equals,那就看你自身是如何定义的了。在String中的equals判断的是值是否相同。
String s = "qwe";
String ss = new String("qwe");
System.out.println(s == ss);
System.out.println(s.equals(ss));
用这段代码就能看的出来
HashCode和equals
- 什么是HashCode
在java中通过使用hashCode()获取哈希码(散列码);在Java中的任何对象都有哈希码(其实就是通过计算得到的一个整数) - 不会创建类对应的散列表
在这个情况中hashCode()与equals()没有任何关系。因为euqlas是通过自己重写的规则来判断两个对象是否相等的。 - 创建了对应类的散列表。这种情况下hashCode和equals是有关系的。
- 如果两个对象相等(equals返回值为True)那么,这两个对象的hashCode()一定相同
- 如果两个对象的hashCode相同但是他们不一定相等,此时就会出现哈希冲突
- 这是个重要的知识点,那么我就用逻辑来说明吧
前提是:在每一个重写了equals方法的类中都要重写hashCode方法。为什么?因为在散列这个对象的时候,是hashCode来确定这个对象在散列表中的位置的,如果遇到哈希碰撞,就会用euqals比较,如果euqals返回是true就认为这个值已经添加过了,如果是false,就会认为这个对象还没有加入,那么就会再散列一次。
final须知
- 被final修饰的变量不能改变
- 被final修饰的方法不能被子类重写
- 被final修饰的类不能被继承。(final类中的方法自动为final,你类都不能被继承了,也不存在重写了)之所以用final修饰类,是为了证明这个类起码目前还是比较完善的,不想被改变它的原意
字节流和字符流
Java中有字节流和字符流。字符是通过Jvm转换字节流得到的,比较耗时,而且可能出现乱码的情况。提供一个字符流直接操作字符。
但是一般对于图片、视频、音频还是使用字节流。
深拷贝和浅拷贝
浅拷贝:复制一个地址
深拷贝:新的地址,但是成员变量的值和原来的相同。
Java中的集合部分
-
List、Map、Set的区别
List是一个有序的集合,且集合中的元素可以重复
Set是一个无序的集合,且集合中的元素不可以重复
Map一个键值对的集合,一对一的映射。 -
所有集合
- LIst
- LinkedList
- ArrayList
- Vector
- Set
- HashSet
- TreeSet
- LinkedSet
- Map
- HashMap
- TreeMap
- Hashtable
- LinkedHashMap
- LIst
-
ArrayList和LinkedList的区别
- ArrayList的底层是数组,而LinkedList的底层是链表
- ArrayList支持随机访问,而LinkedList不支持随机访问
- ArrayList扩容比较苦难,而LinkedList扩容比较简单
- ArrayList插入头部(或者随机插入)的时间复杂度为O(n),插入尾部是O(1)。LinkedList插入头部是O(1),插入(或者随机插入)尾部是O(n)。
- LinkedList的每一个节点都会比ArrayList的节点大。而ArrayList会浪费一部分存储空间(因为自动扩容的关系)
- ArrayList的扩容机制,一开始是10,后面放大因子是1.5
-
ArrayList和Vector
Vector是线程不安全的ArrayList
在这里提一嘴,什么是线程安全,就是一块数据在同一个时刻,只能被一个线程访问。而线程不安全就是一个数据在一个时刻能被不同的线程访问到。 -
HashSet和TreeSet和LinkedSet的底层实现
- 首先三者都是线程不安全的
- HashSet的底层是一个HashMap实现的。
- LinkedList底层是一个链表+哈希表
- TreeSet的底层是一颗红黑树
-
HashMap和HashTable的区别
- HashMap是线程不安全的,而HashTable是线程安全的
- HashTable基本被淘汰了。
-
HashMap的底层实现
- 在jdk1.8之后,HashMap变成了一个哈希表加上一个链表或者红黑树。当链表的长度超过8的时候,这个链表就由红黑树代替。
- HashMap的默认大小是16,负载因子为0.75,每次扩容变为原来的两倍。
-
HashMap和HashSet的区别
- HashSet的底层是一个HashMap
-
HashSet是如何实现去重的
- 加入元素的时候计算HashCode,如果不存在相同值HashCode的元素就直接插入,如果存在就用euqals比较,如果euqals比较是相同的默认这个元素相同
-
HashMap的线程不安全问题
- jdk1.7之前多线程会造成死循环(环形链表)的问题
- jdk1.8解决了死循环的问题,但是可能会有丢失修改的问题发现。
-
ConcurrentHashMap和HashTable的区别
- ConcurrentHashMap在1.7的时候是分段锁,把数据分成segment,对不同的segment段加锁。
- ConcurrentHashMap在1.8的时候没有segment的概念,改用synchronized 和 CAS 来操作.
- CAS操作:CAS 有三个 数值 分别是V(内存值),旧的预期值A、和期待插入值B。如果V==A就把V更新为B。
多线程部分
-
Java实现多线程的两个方法,一个是继承Thread类一个是实现Runable接口。Thread类有构造函数能接受实现了Runable接口的类。
-
sleep()和wait()的区别
- sleep()是占锁休眠,可以理解为是进入了就绪状态,具备了除了cpu的所有资源,而wait()是不占锁的。
- sleep()之后程序是自动苏醒的,不需要被唤醒。而wait()的进程需要被唤醒。
-
直接使用run()方法和先使用start()再使用run()的区别
直接使用run()相当于在main()方法下调用一个方法,不是多线程,而先start()再run()相当于线程准备好了,等待cpu分配时间片,分配到时间片后开始运行,是多线程 -
手写一个死锁案例
public class Lock {
private static Object resource_1 = new Object();
private static Object resource_2 = new Object();
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread()+"尝试获取resouce_1");
synchronized (resource_1){
System.out.println(Thread.currentThread()+"成功获取resouce_1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"尝试获取resouce_2");
synchronized (resource_2){
System.out.println(Thread.currentThread()+"成功获取resouce_2");
}
}
},"线程1").start();
new Thread(()->{
System.out.println(Thread.currentThread()+"尝试获取resouce_2");
synchronized (resource_2){
System.out.println(Thread.currentThread()+"成功获取resouce_2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"尝试获取resouce_1");
synchronized (resource_1){
System.out.println(Thread.currentThread()+"成功获取resouce_1");
}
}
},"线程2").start();
}
}
- 何为synchronized
- 这个修饰符出现,是为了加锁。他是为了保证他修饰的代码块,他修饰的方法,在同一个时刻只有一个线程在执行。
- 使用方法
synchronized static void method(){} //修饰静态方法锁是加在类对象头上的。 synchronized void method{}//锁记载实例的头上 synchronized(resource){}//只有获得了resource资源的,才能执行这个代码块,且只有一个线程能获得
- 手写一个同步锁的案例
public class Lock2 implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (this.ticket > 0) {
//休眠1s秒中,为了使效果更明显,否则可能出不了效果
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "号窗口卖出:" + this.ticket-- + "号票");
}
}
}
public static void main(String[] args) {
Lock2 demo=new Lock2();
//基于火车票创建三个窗口
new Thread(demo,"a").start();
new Thread(demo,"b").start();
new Thread(demo,"c").start();
}
}
这个案例中,ticket是线程不安全的,可以看到,会出现-1张票的情况发生,为什么,因为进入代码块的时候,假设此时ticket = 1,线程a使用过后打印1 ticket = 0,线程b使用过后打印0 ticket = -1这个道理
public class Lock2 implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
synchronized (this){
if (this.ticket > 0) {
//休眠1s秒中,为了使效果更明显,否则可能出不了效果
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "号窗口卖出:" + this.ticket-- + "号票");
}
}
}
}
public static void main(String[] args) {
Lock2 demo=new Lock2();
//基于火车票创建三个窗口
new Thread(demo,"a").start();
new Thread(demo,"b").start();
new Thread(demo,"c").start();
}
}
- 为什么要使用线程池
- 避免创建和销毁一个线程带来的开销。
- 提升相应速度
- 相对应的维护这个线程池也是需要消耗代价的。
- Runnable接口和Callable接口的区别
- 有没有返回值
- 能不能抛出异常
浅说一下JVM
- 什么是JVM。它是一个真的虚拟机,运行在windows或者Linux的操作系统之上
- 垃圾回收机制,就是凭借这个对象被使用的次数来移动它至新生代和老年代直至被销毁。
- 一个.java文件的运行过程
- Java文件经过编译后变成.class字节码文件
- 字节码文件通过类加载器放入JVM虚拟机中
- 通过JAVA虚拟机运行
- 废弃常量和无用类
无用类的表明- 所有实例都被回收
- Class.loader被回收
- 没有java.lang.Class被引用。
- 两种垃圾回收机制分别是分代复制垃圾回收机制和标记垃圾回收机制
浅说一下泛型
-
泛型的使用
- 在类上定义泛型。那么此时这个类更像是一个工厂,可以根据这个模板产生不同类型的实例
public class Pair<T>{ T first; }
-
方法旁边加入泛型
public static<T> T getMiddle(T... a){}
现在使用的 T… a是一个可变长的参数。如果里面放了不同类型的对象就会去自动去找他们的超类。比如放了 1 和1.1 一个int 和一个double对象,就回去找他们的超类变成了Number也是一个泛型。此时就会报错哦。
-
类型擦除
在JVM中 List<Integer>和List<String>是没有区别的。他们都会被定为List<Object>
当然这个是可以规定上限的,通过List<T extends String>规定上界。
一般来说向List<Integer>这样的数据是不能add String的,但是可以通过反射绕开这个限制,达到一种便器不能达到的效果。
反射
反射之所以这么重要,是因为它赋予了我们在运行时分析类和执行类中方法的能力。
- 获得Class对象的四种方法
- xxx.class
- Class.forName(“com.ice.xxx”)
- xxx.getClass()通过实例对象获得
- ClassLoader.loadClass(“com.ice.xxx”)
- 可以通过这个直接制造实例如xxxClass.newInstance();
- 获得反射对象的方法的几种途径(前提是你已经取得了目标类的Class对象c)
c.getDeclaredMethod()
c.getMethod()
- 获得反射对象的属性(前提是获得了目标对象的Class对象c)
c.getFiled()
讲解一下在java中的内存
首先说明,为什么Java是半解释半编译语言
编译语言,把代码全部翻译成计算机指令,然后让电脑去运行
解释语言,一行一行,把代码翻译成指令运行
java 先翻译成.class文件,然后交给jvm一条一条运行
-
首先了解一下对于Java来说内存意味着什么。
从操作系统的角度我们能够看到,内存意味着一个快速访问的存储器,并且附带了很多程序装入内存的方法,或者是一些虚拟存储技术。但是从java执行一个程序的的角度(相当于从Java进程的角度)看内存又是一个什么样子的呢。
就是上面这张图这样的。
可以粗暴的把Java的内存线程私有的和线程共享的两块。线程私有的内容有- 程序计数器:老朋友了,记录这个线程的指令运行到哪一个指令了,这样上下文切换的时候,也能根据这个计数器,顺序执行命令。
- 本地方法栈:执行一个本地方法?没太理解
- 虚拟机栈:存放局部变量,一些方法的信息
-
不共享的堆内存,存放对象实例,几乎所有的对象实例以及数组都是在这里分配的。也是垃圾管理的主要管理区域也被称作GC堆。
堆内存一般被分为- 新生代
- 老生代
- 永久代
-
垃圾回收技术
- 现在用的技术大部分都是分代垃圾收集算法。 其次还有标记垃圾回收算法。
JVM相关
Java的内存划分
感觉这张图不够清晰哈
内部的组分称为
其中线程独有的有:1. 虚拟机栈 2. 本地方法栈 3. 程序计数器
先讲线程,再讲整体吧
- 元空间:它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据,而元空间是方法区的实现类,元空间里面存放的是类的元数据
- 直接内存:因为java没有读取磁盘的能力,读取磁盘就要切换系统态,读取完之后还要再转换为用户态。为此直接在操作系统开辟共享内存,数据直接映射,减少系统调用。
- 堆内存:首先,对内存是线程全员共享且内存最大的一块区域。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。所以这里也是垃圾回收机制发生最频繁的地方。
- 虚拟机栈:这个栈主要由栈帧组成。
- 一个栈帧的组成:
- 局部变量表:主要存放一些变量,包括int long这种基本变量和一些引用变量比如Object(这个存的是地址)
- 操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
- 动态链接:略
- 方法返回地址:方法的结果返回值
- 本地方法栈:与虚拟机栈相似,只不过一个调用的是java,一个调用的是本地方法。
- 程序计数器:为什么要有程序计数器呢,众所周知,线程是要不停的切换上下文的,切换上下文的话,而程序计数器就是记录下一条指令是啥,这样切换回来的时候就知道执行什么命令。
垃圾回收机制
- 大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,就是在青年去清除一部分对象。只有当对象过大,Eden没有足够空间的情况,会采用分配担保机制,确保老年区的空间足够大,能装得下这个对象,再将对象 装进去,不然就直接OOM,内存溢出。
- 对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。随后的每一次Minor GC年级增大1岁,直到15岁进入老年区
- 正对GC,垃圾回收,其实只有两种,分别是partition gc(比如针对老年代的和年轻代的gc),部分垃圾回收和full gc全堆垃圾回收
- 在gc的时候要做对象死亡判断
- 方案一:计数器。通过计数器记录对象被引用的次数,如果被引用的次数为0就被认定为死亡。可是这样带来的问题是,如果存在互相引用的对象,这样这辈子就不会被回收了
- 方案二:可达性判断。从GC ROOT能否到达这个对象(相当于一个二叉树遍历),哪些可以做root呢,比如被类中静态属性引用的变量。
- 第一次被判定死亡的对象会进入一个队列,在第二次还是依旧被判定死亡才会被回收。
- 无用常量:常量池里有数据,但是没有引用
- 无用类:class.load被回收,没有xx.class的调用(这样不能通过反射),没有对应的实例,这样的类就是无用类
- 垃圾收集算法
- 标记-清除算法:最核心的算法,其他算法就是对他做了演进。首先标记出所有不需要回收的对象,然后把没吃到标记的对象释放。
- 标记-复制算法:把堆内存分为两块,同一时间段,只有其中一块记录素具,标记的流程依旧相同。标记的内存块进入另一块未被使用的内存区域中(按顺序
- 标记-整理算法:内存还是一块,但是被标记的内存会移动,自动组合成一段比较完整的内存空间。
- 为什么要分新生代和老年代?因为老年代的对象证明用的频率比较频繁,不应该去回收,而新生代相对来说使用频率没有老年代这么高,因此区分开来,也方便采用不同的垃圾回收算法来对这两个区域做处理。
类的加载过程
- 我们都知道我们一般编写的是java文件xx.java通过javac被编译成xx.class文件,再通过类加载器装入jvm,jvm再帮助我们执行。
- 类加载的过程如下
- 首先在加载阶段,主要是在内存中生成一个Class对象,作为方法区的一个接口。
- 在连接阶段主要做一大堆检查,比如语法检查,元数据检查,并且生成一些静态变量,会对这些静态变量赋初始值。这个初始值比如boolean = false。
- 初始化,这个时候才对做一些初始化,相当于执行init方法
- 最后使用,再通过垃圾回收机制回收
类加载器
- 双亲委派机制:所谓的双亲委派机制,就是当存在父类的类加载器,会把这个类的加载交由父类完成,当父类不能完成这个类的加载,才由子类来进行加载。
- 可以做数据隔离,比如我们自定义一个Object对象,但是由于类加载器已经加载过Object对象了,就不能再次加载,这也是变相的数据隔离。也是做到了避免重复加载
- 可以做数据隔离,比如我们自定义一个Object对象,但是由于类加载器已经加载过Object对象了,就不能再次加载,这也是变相的数据隔离。也是做到了避免重复加载
高并发的一些问题
- 多线程比多进程的优点
- 因为线程颗粒度比较写,切换开销就小了。这样支持的并发度一定是高于进程。
- 对于一个cpu运行一个进程,如果进程阻塞了,那整个进程都阻塞了,而进程中一个线程阻塞了,其他线程还是可以运行的。
- sleep() 和 wait的区别
- sleep() 是不放权等待,而wait()是放权等待。
- wait用于线程之间的通信,而sleep()用于暂停这个线程的执行
- 执行了wait()的线程需要被唤醒,而sleep()时间到了会自动苏醒。
- 因为wait涉及到的是Object对象锁,所以设计在Object()之中。而sleep()涉及到的是线程的暂停,因此设计在Thread之中
- new Thread().run()。执行run()相当于在main这个主线程下执行流程。而new Thread().start()是重新启动一个线程,并处于就绪状态,等到分配到时间片就能运行。
- volatile 关键字能保证数据的可见性,因为这样禁止cpu使用缓存,每一次拿数据都是从主存中去拿,但不能保证数据的原子性。synchronized 关键字两者都能保证
- synchronized的使用方法
- 修饰实例方法:修饰方法意味着执行这个方法需要先获得这个对象的锁
- 修饰静态方法:获得这个class对象的锁,获得所有这个类创建实例的锁
- ThreadLocal就是用于创建属于这个线程自己的变量
- 使用(线程池)池化技术的好处
- 降低创建销毁线程带来的损耗
- 提高响应速度
- 提升对于线程的管理能力
- Runnable和Callable的区别在于会不会返回值和抛出异常
- AQS
补充!!
HashMap
HashMap在jdk1.7之前,采用的底层数据结构为数组+链表,插入元素是头插法插入数据,在并发的情况下扩容,会导致链表成环的情况
在1.8之后
HashMap 是数组+链表+红黑树 组成。但是还是有并发问题,当多个线程插入,会造成数据丢失的问题。在过度哈希冲突的情况下,会将链表转换成红黑树。
红黑树的数据结构
红黑树的本质是一个自平衡的,通过左旋右旋达到平衡。
HashTable是一个遗留类。他是线程安全的,但是没有用分段锁,所以他的并发不行。如果想要现成安全,可以替换使用ConcurrentHashMap
ConcurrentHashMap底层用synchronized将数据分成一段一段的对一段数据加锁
CAS比较替换
A旧值
V当前值
B新值
只有 A == V时才能替换成B
synchronized
获取对象锁的两种方式
- 同步代码块(synchronized(this) , synchronized(类实例对象)),锁的是小括号()中的实例对象
- 同步非静态方法(synchronized method),例如放在public后面,锁的是当前对象的实例对象
获取类锁的两种用法 - 同步代码块(synchronized(类.class) ),锁得是小括号()中的类对象(Class对象)
- 同步静态方法(synchronized static method),锁的是当前对象的类对象(Class对象)
之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
四种状态
无锁状态
偏向锁
轻量所
重量锁
HashMap HashSet CurrentHashMap
HashTable是线程安全的,这是因为HashTable在方法中,都使用的Synchronized来修饰,保证线程安全。
HashMap,如果不加初始容量的话,初始容量为16,之后的每一次扩容都变成原来的两倍。发生哈希冲突后,是采用链表来进行解决的,而链表超过8之后会自动转换为红黑树。红黑树是一颗不太标准的字平衡树,它允许局部的不平衡,这样虽然损失了一部分查找性能,但是增加和删除的性能得到了提高。
HashSet底层是调用HashMap来实现的,基本上用的都是HashMap的方法。
为什么HashMap会线程不安全?
- 在Java1.7之前,是使用数组+链表的形式来完成HashMap的,使用头插法,在扩容截断,两个线程交替放入会有环形链表的问题。
- 而Java1.8虽然改用了尾插法,但是还是有数据覆盖的情况产生,链表成环的问题是通过双指针来解决的
HashMap中数组的长度,通常是2的幂次方,这是因为尽量减少哈希碰撞。
CurrentHashMap:在java1.7之前,使用的是分段锁。在之后,使用的是Node+红黑树+Synchronized和CAS操作来保证线程安全的,颗粒度更小。
HashMap默认的负载因子为0.75。负载因子过小,频繁的扩容带来性能的损耗,负载因子过大哈希冲突就会很多。