类型 | 数据类型和占用字节 |
---|---|
整数型 | byte(1字节) short(2字节) int(4字节) long(8字节) |
浮点型 | float (4字节) double(8字节) |
字符类型 | char(3字节) |
布尔型 | boolean(1字节) |
数组因为在物理内存上的连续性,当要增删元素时,会导致后续元素都要移动一位,所以效率比较低,而链表则可以轻松的断开和下一个元素的连接,直接插入新元素或者移除旧元素效率较高,但增删在尾部的时候,不需要移动后续元素,所以 ArrayList 比较快。
单向链表
组成:链接方向是单向的,链表的遍历要从头部开始顺序读取;结点构成,head指针指向第一个成为表头结点,终止于最后一个指向NULL的指针。
优点:单向链表增加删除节点简单,遍历时候不会死循环。
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。
双向链表
组成:双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
优点:可进可退,能找到前驱和后继。
缺点:占用内存较大,需要多分配一个前驱指针,遍历复杂。
双向循环链表
组成:双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个循环,LinkedList就是双向循环列表组成的。
优点:从循环链表中的任何一个结点出发都能找到任何其他结点。
JDK 1.7
存储结构:数组+链表
插入方式:头插法
扩容方式:先扩容,再添加
扰动函数:要进行4次位运算 + 5次异或运算
JDK 1.8
存储结构:数组+链表+红黑树
插入方法:尾插法
扩容方式:先添加,再扩容
扰动函数:进行1次位运算 + 1次异或运算
为了解决hash碰撞后,链表过长引起的查询效率问题。
数组的长度大于64,链表长度大于8才会从链表转换为红黑树。
当数组长度小于64时,数组+链表比红黑树占用空间较小,而且红黑树自平衡比较耗时,hash碰撞严重时不如先扩容来解决冲突。
1.先判断数组是否为空,为空则调用resize()进行扩容,直接插入到对应的数组中。
2.不为空则根据当前key来计算数据的索引值判断当前key是否存在,如果存在,则覆盖value值。
3.不存在则判断是否是红黑树,是红黑树结构则直接插入。
4.不是红黑树的话,遍历链表准备插入,若链表长度小于8且数组长度小于64时,直接插入链表,数组的长度大于64,链表长度大于8的时候,则将链表转为红黑树,转成功之后,再插入。
5.判断++size是否大于threshold,大于则进行一次resize();
1.7
存储结构: Segment数组 + HashEntry数组 + 链表
线程安全实现:Segment继承了ReentrantLock,用来做分段锁,理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。
分段锁:ConcurrentHashMap由多个Segment组成(Segment下包含很多Node),每个Segment都有一把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
1.8
存储结构: Node数组 + 链表/红黑树
线程安全实现:取消了分段锁的概念,采用synchronized关键字和CAS来保证其线程安全,CAS失败自旋保证成功,再失败就synchronized保证。
synchronized:只锁定当前链表或者红黑树的首节点。
CAS(compare and swap):CAS 操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么会自动将该位置值更新为新值。否则,不做任何操作。
synchronized关键字用来来防止线程干扰和内存一致性错误,解决了多线程之间资源的一致性。
对象中内置一个Monitor监视器
Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。
对象在内存中由三个部分组成
分别为:对象头,实例数据和对其填充(非必须)构成。
Mark Word:存在于对象头中,存储对象的hashcode、锁信息、分代年龄和GC标志等信息,锁升级的过程就是基于修改Mark Word来实现的。
如何实现同步
同步方法:是隐式的,无须通过字节码指令来控制,通过ACC_SYNCHRONIZED访问标志判断一个方法是否声明为同步方法。
同步代码块:通过monitorenter和monitorexit两条指令来实现同步,关联到一个monitor对象,进入时设置Owner为当前线程,进入计数器+1、退出计数器-1。
过程:无锁状态->偏向锁状态(1次CAS)->轻量级锁状(多次CAS)->重量级锁状态
锁升级的过程是不可逆的,锁升级是为了将众多细粒度锁转换为较少的粗粒度的锁的过程,以削减系统开销,降低锁带来的性能消耗。
volatile是Java提供的轻量级的同步机制
一种缓存一致性协议,主要来解决多核CPU访问内存比较慢和缓存不一致等问题。
状态 | 描述 |
---|---|
M(Modified) | 数据被修改了,和内存中的数据不一致,数据只存在于本Cache中 |
E(Exclusive) | 数据和内存中的数据一致,数据只存在于本Cache中。 |
S(Shared) | 数据和内存中的数据一致,数据存在于很多Cache中。 |
I(Invalid) | 缓存行中的内容时无效。 |
1.所有的变量都存储在主内存中,每个线程都有一个私有的工作内存来存储共享变量的副本,并且每个线程只能访问自己的工作内存,无法访问其他线程的工作内存,也不能直接读写主内存中的变量。
2.不同线程之间无法直接访问对方工作内存中的变量,线程间变量值传递均需要在主内存来完成。
由于volatile基于MESI缓存一致性协议需要不断的从主内存嗅探和CAS不断循环无效交互导致总线带宽达到峰值。
基于volatile + CAS + Native 来实现的线程安全的int包装类
Lock锁也叫做同步锁,实现类为ReentrantLock。
synchronized是关键字,作用于JVM层面的,Lock是API层面的锁。
synchronized不需要手动释放锁,在线程结束或者异常时会自动释放锁。ReentrantLock必须手动去释放锁,遇到异常,释放不正确会导致死锁,一般在finally{}中释放。
synchronized为非公平锁,ReentrantLock可以通过参数来设置是否为公平锁,是否公平取决于线程是否按照先后顺序获取锁。
synchronized不可中断,ReentrantLock可以中断。
ReentrantLock提供的Condition(条件)可以指定唤醒哪些线程,而synchronized只能随机唤醒一个或者全部唤醒。
ReentrantLock比synchronized更灵活。
ThreadLocal类存储每个线程的私有数据,即线程的局部变量副本,每个线程维护自己的一份数据,从而达到线程隔离的效果。
ThreadLocalMap的生命周期与 Thread 一致,同时ThreadLocalMap 中的 Entry的 key 对 ThreadLocal 是弱引用,但是Entry中的value是被Entry强引用的,如果不手动的清除掉 ThreadLocalMap中Entry对象,在垃圾回收时,key会被回收掉,但是value会被回收,这样不管是否使用弱引用都有内存泄露的可能。所以一般在使用完后需要手动调用remove()方法清除掉。