HashTable 是线程安全的。HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下.
数据结构和原理使用ConcurrentHashMap ,分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment(JDK1.7)
JDK 采用自旋锁+锁膨胀的机制进行加锁,也就是自旋获取锁,当自旋次数超过64时,会发生膨胀,直接陷入阻塞状态,等待唤醒。并且在整个put *** 作期间都持有锁。
JDK1.8中取消了Segment 分段锁,采用 CAS + synchronized 来保证并发安全,ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发
只需要一次定位,如果对应下标处没有结点,说明没有发生哈希冲突,此时直接通过CAS进行插入,若成功,直接返回。若失败,则使用synchronized进行加锁插入。
锁的粒度从段锁缩小为结点锁,并且不是每次都要加锁了,cas尝试失败了再加锁
put()方法中 初始化数组大小时,1.8不用加锁,因为用了sizeCtl这个变量,为-1时表示 table正在初始化,-(n+1)时表示正在扩容,正数表示扩容时的阈值
synchronized是jvm层面的锁,自动释放锁
reentrantlock是api层面的锁,需要手动unlock()释放锁
reentrantlock
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
1.7、1.8区别1.7 | 1.8 | |
---|---|---|
数据结构 | ReentrantLock、Segment、HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构 | Synchronized、CAS、Node、红黑树,Node的val和next都用volatile修饰,保证可见性,查找替换赋值都用CAS |
扩容 | segment每个元素内部进行扩容,不影响其他的segment元素 | 如果某个线程put时发现concurrenthashmap正在扩容时,那么改线程会一起进行扩容,如果没有扩容则吧key value添加到map中,然后判断是否超过了阈值判断是否扩容 |
支持多个线程同时扩容,扩容之前也会生成一个新的数组,每个线程负责单独的部分转移 | ||
元素查询 | 两次hash,第一次定位到segment,第二次定位到元素所在链表的头部 | 查找替换赋值都用CAS(性能较高) |
加锁 | put对于当前的segment加锁,get无需加锁,volatile保证可见性,Node用volatile修饰 | 锁链表的head结点,不影响其他元素的读写,粒度更细,效率更高,扩容时,阻塞所有的读写 *** 作、并发扩容,读 *** 作无锁,volatile保证可见性,val和next都用volatile修饰,数组也用volatile修饰 |
可以修饰类、方法、成员变量、局部变量
修饰类表示类不可被继承
修饰方法表示方法不能被重写,但是可以重载
修饰变量表示一旦被赋值不能更改为其它值
修饰成员变量1.如果是普通成员变量只能在普通代码块内初始化,构造方法中初始化或声明时初始化值
2.如果是类变量(静态成员变量),可以在静态代码块中初始化,或声明时初始化
修饰局部变量必须由程序员显式的初始化,在使用之前进行初始化(只能初始化一次)
修饰基本数据类型数值一旦被初始化不能修改
修饰引用数据类型在初始化后,其引用的地址不能变(不能指向另一个对象)。但是引用的值可以改变
为什么局部的内部类和匿名的内部类只能访问局部final变量
编译之后会生成生成两个class文件,Test.class、Test1.class
可重入锁,可以在锁的内部再加锁(加几次锁,释放几次锁)
可以实现公平锁,也可以实现非公平锁
公平锁和非公平锁都使用AQS
公平锁 | 非公平锁 |
---|---|
加锁时,检测AQS是否有线程排序,如果有则,当前线程到AQS排队 | 加锁时,先判断时候加到锁,没有竞争到锁则到AQS排队 |
有线程排队则唤醒排在AQS前面的线程 | 没有竞争到锁会唤醒排在AQS前面的线程 |
AQS就是并发包下的一个基类
AbstractQueuedSynchronizer类中,有几个属性和一个双向队列 (CLH队列)
内部类
static final class Nodes {
//排他锁的标识
static final Node EXLUSIVE = null;
//如果带有这个标识,证明是失效了
static final int CANCELLED = 1;
//具有这个标识,说明后继结点需要被唤醒
static final int SIGNAL = -1;
//Node对象存储标识的地方
volatitle int waitStatus;
//指向上一个结点
volatitle Node prev;
//指向下一个结点
volatitle Node next;
//当前Node绑定的线程
volatitle Thread thread;
//返回前驱加点,如果前驱加点为null,抛出NPE
final Node predecessor() throws NullPointerException {
Node p = prev;
if(p == null){
throw new NullPointerException();
}else{
return p;
}
}
}
双亲委派机制
jvm理解
java源程序->.class(字节码文件对象)->jvm运行
什么是OOM
什么是栈溢出StackOverFlowError?怎么分析?
内存快照如何抓去,怎么分析Dump文件?
谈谈JVM中,类加载器你的认识
1.jvm的位置jvm是位于 *** 作系统之上的
2.jvm的体系结构 3.类加载器的作用加载Class文件
类加载器- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
- 自定义类加载器
每个加载器都有属于自己的加载范围如根加载器(java_home下的rt.jar)扩展类加载器 (java_home下jre的ext文件夹)
双亲委派机制:当一个加载器收到类加载请求会传递给他的父亲加载器加载,因此最终都会从根加载器加载,然后加载器会检查是否能加载该类,否则抛出异常通知子加载器加载 -------》默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,它是唯一的,不会产生歧义
ArrayList和LinkedList 数据结构ArrayList是用一个长度数组实现
LinkedList使用链表实现
使用场景因为ArrayList用数组实现,用下标可以直接访问元素,所以查询效率很快,适合查询频繁的情况,但因为其空间连续,删除和插入元素会让部分元素移动位置,进而消耗资源,所以不适合插入、删除频繁的情况
LinkedList用双向链表实现,删除和插入元素直接修改结点的指向就行,所以插入、删除效率很高,适合插入、删除频繁的情况,但是查找元素会遍历整个链表,消耗资源所以不适合查询频繁的情况
空间LinkList需要更多的内存空间,因为它除了要存储数据之外,还需要存储该节点的前后节点信息,而ArrayList索引处就是存的数据
ArrayList扩容初始化容量为10,第一次添加元素的时候才会初始化容量为,默认为10
每次扩容时原来的1.5倍,会将老数组中的元素重新拷贝一份到新的数组中
区别ArrayList | LinkedList | |
---|---|---|
数据结构 | 长度可变的数组 (扩容影响效率) | 双向链表(存指针,更占空间) |
连续空间(随机访问快速) | 可以不连续 | |
实现接口 | 实现List | 实现List、Dequeue(可实现双端队列) |
使用场景 | 随机访问频繁 | 插入,删除频繁 |
使用尾插法并指定初始容量可以极大提升性能、甚至超过LinkedList | 需要创建大量Node结点,损耗空间 |
static final ThreadLocal<T> localVar = new ThreadLocal<T>();
//设置线程中本地变量的值
localVar.set()
//获取线程中本地变量的值
localVar.get()
//清除本地内存中的本地变量
localVar.remove();
作用
使用Threadlocal绑定的变量,会将其缓存再线程内部,线程访问改变量时访问的是缓存的内容
每个线程对象内部都有一个ThreadLocalMap key为ThreadLocal对象,value为缓存的值
当我们在创建一个变量后,每个线程对其进行访问的时候访问的都是线程自己的变量
线程隔离
内存泄露问题使用线程池时,线程不会被销毁,内部缓存的对象不会被垃圾回收,如果不断创建这样的对象,会造成内存空间被一步一步侵蚀
解决1.在使用完毕后应该用remove方法清楚本地内存中的本地变量
2.将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
场景1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务 *** 作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务
过程都是使用该线程绑定的connection来执行数据库 *** 作,实现了事务的隔离
性。Spring框架里面就是用的ThreadLocal来实现这种隔离
对象创建的过程
对象创建的过程
.java----javac---->.class-----ClassLoader
1.加载类到内存
2.半初始化
3.init
由类加载器把类加载进内存,
初始化过程,主要是对目标类里面的静态变量,成员变量,静态代码块进行初始化,当目标类被初始化后就可以从常量池里找到类元信息了
之后jvm会把目标对象里的普通成员变量初始化为0值
(int初始化为0,string初始化为null)
执行目标对象内部生成的init方法,进一步初始化成员变量的值
执行构造块,最后调用对象的构造方法
对象在内存中的存储markword 8
classpointer 4
instance data
padding
Object o = new Object();占多少字节?
16字节,因为makword占8字节,classpointer占4字节,没有成员变量所以没有实例数据,然后对齐4字节
hashmap 哈希算法根据任意长度计算出固定长度的值
好的哈希算法1.高效计算出目标值,长文本也能快速计算出hash值
2.根据hash值很难推出原文
3.尽可能散列
4.原始信息一点不同,hash值都有很大区别
数据结构1.8是数组+链表+红黑树
每个数据单元是一个node结构
node结构有key、value、next、hash字段
默认初始散列表数组长度为16 散列表(数组)是懒加载在第一次put时才会创建数组 默认负载因子0.75 用来计算扩容的阈值 默认情况下扩容阈值是12(16*0.75) 链表转红黑树 链表长度大于8,数组长度大于等于64时进行树化 怎么计算hash值计算key的hash值,高16和低16异或运算
put过程四种情况- slot为null,则直接把key和value包装为node放入slot
- slot不为null,则判断key是否完全相等,相等的话就调用replace方法替换,没有链化的话,不等就尾插法追加,如果已经链话则,继续比较下一个结点,如果key完全相等则替换,最后如果到最后一个结点不想等则追加,然后判断时候打到阈值,判断时候进行树化
到达阈值后进行左移运算
不*2因为位运算性能高
hashmap1.7和1.8 共同
先根据key计算出hash值与数组长度-1相与,计算出在数组中的下标
区别1.7 | 1.8 | |
---|---|---|
数据结构 | 数组、链表 | 数组、链表、红黑树 |
结点 | 封装为Entry | 封装为Node |
put过程 | 根据key计算出下标,则判断数组位置是否为空,为空则直接放入元素,不为空则认定为一个链表,遍历链表如果遇到key相同的就直接更新 value,如果没有相同,之后判断是否需要扩容需要扩容则扩容到原来的两倍,之后采用头插法插入 | 根据key计算出下标,则判断数组位置是否为空,为空则直接放入元素,不为空则判断当前结点为链表还是红黑树(为链表则封装为链表结点,为红黑树则封装为一个红黑树结点),遍历链表或红黑树如果遇到key相同的就直接更新 value,如果没有相同key则采用尾插法插入元素,最后判断时候进行扩容,或者树化 |
扩容 | 树化当链表结点数>8,数组长度大于等于64时,将当前链表转化为红黑树(当结点数<=6时退回链表),扩容当hashmap中元素个数>=阈值(数组长度*0.75)时进行扩容,长度左移 | |
插入元素 | 头插法,插入前判断是否扩容 | 尾插法,插入后判断时候扩容 |
hash算法 | 较为复杂 | 简化,因为红黑树提高了插入查询效率,可以简化hash算法节省资源 |
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)