HashMap底层是JDK7的时候时 Entry数组 + 链表,而JDK8中则为 Node数组+链表+红黑树(当链表的长度大于8的时候自动转换成红黑树,链表长度小于6的时候恢复成为链表,之所以这么中间有个过度值是因为,这样能够减少频繁转换所带来的CPU资源的消耗)。
HashMap线程不安全主要发生在扩容和put的时候导致数据不一致
(1)Put的时候造成线程不安全
线程1首先到了B的位置的时候时间片就用完了,然后切换成线程B并且完成了插入的操作,此时CPU重新调度起线程1,但是线程1依旧持有过期的表头,将会执行B.next = C 的时候就会造成了D数据的丢失,从而造成数据不一致的行为。
(2)resize扩容的时候,可能会出现循环链表(但是在JDK8中换成了尾插法已经将该问题规避掉)这里主要介绍一下头插法时存在的一些问题。
因为使用头插法,新元素的位置总会出现在链表的头部(新元素指的是旧链表,拉链出来的排在后面的元素)。
举个例子:
综上就是HashMap线程不安全的原因。
HashSet的底层原理就是HashMap,HashSet中所有的Key作为唯一辨识,所有的Value都为同一个值即为null
● JDK1.7中
JDK1.7中ConcurrentHashMap使用的是分段锁的机制,底层是Segement数组 + reentrantLock实现,它的每个Segment 即是一个HashEntry数组,每个Segment继承了ReentrantLock(也正因为如此可以实现读写分离),当多个请求进来的时候锁住的只是一个Segment。采用的是分段锁的机制
整体的数据的结构如下:
可见分段锁机制的使用,在保证数据一致性安全的条件下降低了锁的粒度
在实现Size()统计所有的元素的个数的时候优先采用的是乐观锁的机制,
就是统计每个segment中元素的个数,最后判断segement的总修改次数是否大于上次的修改的总次数,如果大于上次修改的总的次数重新统计,阈值+1, 超过一定的阈值之后会给每个segment进行上锁进行统计。
参考博客: https://www.cnblogs.com/echola/p/11227689.html
● JDK1.8中
jdk1.8中结构稍作改变,取消了分断锁的机制,采用的是synchronize + CAS + Node数组 +红黑树的方式。结构如图:
统计Size()所有元素的时候
ConcurrentHashMap是采用CounterCell数组来记录元素个数的,像一般的集合记录集合大小,直接定义一个size的成员变量即可,当出现改变的时候只要更新这个变量就行。为什么ConcurrentHashMap要用这种形式来处理呢? 问题还是处在并发上,ConcurrentHashMap是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的的难全兴,势必会需要通过加锁或者自旋来实现,如果竞争比较激烈的情况下,size的设置上会出现比较大的冲突反而影响了性能,所以在ConcurrentHashMap采用了分片的方法来记录大小。
参考博客: https://www.cnblogs.com/technologykai/articles/10966606.html
安全失败和快速失败是针对迭代器而言的。
● 快速失败
是在迭代器遍历集合的过程中元素发生了改变,而抛出的concurrentModificationException,原因是迭代器在访问集合过程中使用一个modCount变量,每次在next()之前都会判断这个modCount变量是否与expectedModCount相等,相等则遍历,不相等则快速失败。因为集合发生改变会造成modCount也发生了改变。
默认: java.util 包下的所有类都是快速失败的
● 安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。这样就算原有的集合发生了改变,但是遍历的集合并不在原有的集合所以并不会影响复制出来的集合,所以它是安全的。默认java.util.concurrent包下的都是安全失败的。缺点就是遍历不到集合的新元素。
参考博客: https://blog.csdn.net/tb9125256/article/details/80892859
(1) 开放地址法
线性探测法:
(2) 拉链法(HashMap中使用的方法)
(3) 使用其他的Hash函数再次Hash,直至不冲突
(4)建立公共的溢出区专门存放冲突的值
参考博客: https://blog.csdn.net/yeiweilan/article/details/73412438
Java中所有的异常都源于 java.lang.Throwable下的子类,可以分为两大类
(1) Error ,这种一般是错误,是虚拟机出错了之类的问题所造成,并不是程序本身的问题
(2)Exception: 而exception又分为check异常和unchecked异常,check异常指的是开发过程中可以避免的一些异常必须使用try catch或者throw进行捕获或抛出。二unchecked异常是指的是无法进行捕获得到的比如一些NullPointException、NumberCastException、IndexOfBoundsException
参考博客: https://blog.csdn.net/bornlili/article/details/55505553
原因简单来说是这样:2进制的小数无法精确的表达10进制小数,计算机在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现了误差。
● 同步阻塞I/0(BIO):
同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io现在,但程序直观简单易理解.
● 同步非阻塞I/O(NIO):
同步非阻塞I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持
● 异步非阻塞I/O(AIO)
异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。
● 同步和异步的区别
同步异步关注点在于消息通信机制
同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生
异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发
● 阻塞和非阻塞
阻塞与非阻塞关注的是程序在等待调用结果时(消息、返回值)的状态
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
String底层中使用的是final修饰的char[] 类型的数组,使用final修饰的是默认不可变的,所以它是线程安全的,并且String是保存在常量池中的,一经创建就无法修改,如果对其修改就会在常量池中创建新的对象,所以它是线程安全的。
详细参考博客: https://www.cnblogs.com/651434092qq/p/11168608.html
StringBuilder默认是线程不安全的底层并没有加上同步锁,所以效率比较高
StringBuffer默认线程是安全的底层加上了同步锁机制,这也造成了效率比较低
ArrayList底层默认使用的是一个数组,而LinkedArrayList默认使用的双向链表。
i++,++i不用入栈,直接对原变量进行运算
其他先压入栈中,然后对变量进行操作
最后进行赋值操作
类初始化过程:
(1)父类静态变量的显式赋值的初始化和静态代码块的显式赋值初始化按照顺序来
(2)子类静态变量的显式赋值的初始化和静态代码块的显式赋值初始化按照顺序来
整体过程会调用方法,并且该方法只调用一次,所有静态相关的东西都在里面进行初始化
类实例化过程:
子类的构造器中含有super()方法,所以先会调用父类构造器。实例化顺序如下:
(1)父类的非静态变量显式实例化和非静态代码块的实例化按顺序执行,最后执行构造器
(2)子类的非静态变量显式实例化和非静态代码块的实例化按顺序执行,最后执行构造器
Ps: 会调用多个方法,注意父类中调用的test()非静态方法,实际上前面有个this,而且this指向的是正在初始化的对象。
基本类型: 值传递
对象类型: 地址
Ps: 数组为地址传递,String和基本包装类是对象传递,但是因为底层是final修饰所以是不可变,当对其进行修改的时候会创建的对象。
红黑树是基于二叉排序树而来的,而Java中TreeMap、TreeSet都是基于红黑树来实现的。
红黑树有如下特性:
(1)所有的节点都是黑色或者红色
(2) 根节点必须是黑色
(3)所有的叶子节点都是黑色
(4)没有两个相连的红色节点,即该节点红色他的左子节点和右子节点都为黑色
(5)从任意节点出发到叶子节点经过的路径所包含黑色节点数目相同(因此新增的节点必须为红色节点)
新增元素有三种情况:
(1) 新增为第一个元素成为根节点
(2) 新增到黑色节点下面,不破坏原有的特性,不要做出改变
(3) 新增到红色节点下面,根据具体的当时红黑树的具体情况进行左旋和右旋
详细参考: https://www.jianshu.com/p/e86e0eaa1bec
字符串的intern()方法是判断虚拟机常量池中是否存在值相等该字符串存在则返回引用,
不存在则将其加入到常量池中并且返回该引用。
第二个为啥为false是因为,jdk启动的时候就已经带了这个java的字符串到常量池。