目录
hashmap和hashtable以及concurrentHashMap 区别
反射中,Class.forName(String name)和ClassLoader的区别?
面试题集锦网站
【Java后端面试题整理】 https://my.oschina.net/u/3943244/blog/3008169
1java基础
Linux中的文件描述符与打开文件之间的关系 ☆☆ https://www.cnblogs.com/ginvip/p/6350222.html
谈谈epoll实现原理 ☆☆http://luodw.cc/2016/01/24/epoll/
epoll的底层原理 ☆☆☆epoll的底层原理_epoll底层原理_LG_985938339的博客-CSDN博客
面试题链接
腾讯Java 高级开发岗面试过程[转] https://www.cnblogs.com/davidwang456/articles/14385822.html
Dubbo面试题(2020最新版) Dubbo面试题(2020最新版)-腾讯云开发者社区-腾讯云
Dubbo面试题及答案 Dubbo面试题及答案_LifeBackwards的博客-CSDN博客
写在 19 年初的后端社招面试经历(蚂蚁、头条、PingCAP) 您的访问出错了
OOM分析 Java内存溢出OOM之dump分析_java dump分析_古月化石的博客-CSDN博客
死锁检测 理解Java死锁之死锁检测_检测死锁_叩丁狼的博客-CSDN博客
Synchronize锁升级分析 ☆☆☆Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_tongdanping的博客-CSDN博客 synchronized锁升级分析_synchronized mutex_云原生驿站的博客-CSDN博客
直接内存如何分配,如何回收 https://www.cnblogs.com/zhai1997/p/12912915.html 循序渐进理解Java直接内存回收_迷夏的博客-CSDN博客
CAS底层原理 CAS技术之底层原理_cas底层原理_Jeremy_Lee123的博客-CSDN博客
对象和数组并不是都在堆上分配内存的 ☆https://www.hollischuang.com/archives/2398
面试问我 Java 逃逸分析,瞬间被秒杀了 ☆https://www.cnblogs.com/javastack/p/11023044.html
MYSQL使用B+树的原因 为什么mysql索引要使用B+树,而不是B树,红黑树_索引为什么不用红黑树_wrr-cat的博客-CSDN博客
死锁 死锁的四个必要条件_rabbit_in_android的博客-CSDN博客
对象的访问定位的两种方式 对象的访问定位的两种方式(句柄和直接指针两种方式)_看了个寂寞的博客-CSDN博客
☆☆☆ https://www.cnblogs.com/shyroke/p/8159094.html hotspot 默认用的是直接指针
乐观锁和悲观锁的区别 面试必备之乐观锁与悲观锁_JavaGuide的博客-CSDN博客
rocketmq 如何保证高可用的几种措施 RocketMQ保证高可用和高性能的几种措施 - 知乎
Hystrix熔断机制原理剖析 https://www.cnblogs.com/littlewhiterabbit/p/12918142.html
2pc和3pc等分布式事务一致性讨论 面试官:了解分布式事务?讲讲你理解的2PC和3PC原理 - 知乎
maven 依赖冲突(最短路径+最先声明)Maven项目为什么会产生NoClassDefFoundError的jar包冲突? java - Maven项目为什么会产生NoClassDefFoundError的jar包冲突? - 全栈在路上 - SegmentFault 思否
进程间的通信方式(二):管道Pipe和命令管道FIFO进程间的通信方式(二):管道Pipe和命令管道FIFO_pipe与fifo的区别与联系_MasterT-J的博客-CSDN博客
如何设计秒杀系统
阿里视频 ☆☆ 淘宝秒杀系统怎么设计?——10+后端经验阿里面试官2小时教你处理网站高并发问题_哔哩哔哩_bilibili
OOM分析
在启动的时候需要我们配置jvm参数如下 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://当fillHeap(list,131)时,程序正常执行;当fillHeap(list,132)时,程序就会报OOM异常:
此时我们通过
Java 三色标记法
三色标记法是一般垃圾回收器用来标记不可达对象的通用算法,三色标记算法的核心就是是通过GC root,并通过引用链,将对象标记为黑色,灰色,白色,白色即是不可达对象,需要被清除的
但是在某些情况下面这个算法存在一些问题,
https://www.cnblogs.com/jmcui/p/14165601.html
java hashmap存泄漏有两种原因
1,类似于ThreadLocalMap里面,如果key通过Weakeference包装的话,那么一旦被回收了,那么key就是null,则会导致get不到
2.如果key不是string类型的那种不可变对象,假如它是复杂对象,并且这个key对象里面改变了某个字段的值,并且这个字段还参与了重写的hashcode方法,那么会出现计算出hashcode,那么也是无法查找到对应的node,那么这个也是一个内存泄漏
解决的办法是尽量使用string,Integer这种不可变对象作为key值,或者在计算hashcode的时候屏蔽这种易变的字段参与hashcode计算
HahsMap的优缺点和应用场景
首先hashmap是基于哈希表的一个map实现,内部通过数组+链表+红黑树的形式存放键值对
1优点,增删改查都非常快,时间复杂度是o(1)
2 缺点,内部元素具有无序性,遍历麻烦
3.线程不安全性,hashmap在扩容的时候是很危险的,因为不是线程安全的,如果不加任何锁的话,很可能在扩容的时候,查找不到对应的节点
4.hashmap是惰性加载的,只有第一次putval的时候才会生成节点数组
在很多应用场景下,这种数据结构会搭配策略模式来进行,按照不同的值,选择实现同一接口的不同的策略方式。但是hashmap的内部封装key和value的node节点有一个final的hash,但是无法避免内存泄漏的问题
ConcurrentHashMap扩容
redlock算法问题研究
算法简单描述:给一个锁设置有效时长,redis客户端向集群中的每台主节点去上锁,如果上锁完以后发现成功上锁的节点小于集群中数量的一半,或者锁的有效时间已经小于当前时间,就认为上锁失败,向每台节点解锁。
性能原因:因为redlock需要一个一个主节点去枷锁,势必性能上会有一定的损耗,如果安全性上要求不高的话,一般使用单节点的锁就可以了,因为单节点的分布式锁能处理大部分分布式场景,无需引用redlock。
问题一:redlock如果某些已经成功枷锁的节点在主从同步过程中出现问题,怎么办?
假如线程A在节点abc节点上锁成功,但是在de上锁失败,并且此时a节点master宕机,同步锁信息失败,其slave节点没有锁信息,而此时线程B在a,d,e上锁成功,那么线程A和B都被认为上锁成功。
问题:二:redlock强烈依赖系统时钟,对时间偏移问题无法处理
短网址(short URL)系统的原理及其实现
https://www.cnblogs.com/rsapaper/p/9791855.html
redission看门狗算法
汪~汪~汪~redisson的WatchDog是如何看家护院的?_redisson watch dog会阻塞吗_简熵的博客-CSDN博客
首先redission在节点上第一次加锁的时候设置的过期时间是30秒,在tryLockInnerAsync中异步执行第一次上锁,并返回RFuture,在future定义一个监视器,当第一次上锁执行成功的时候,会启动一个定时任务,这个定时任务是递归的,每10秒看门狗会给锁续时长30秒。
AQS算法描述
1.AQS内部维护了一个int类型的状态变量,同时内部维护了一个双向链表,对于AQS来说,线程的同步关键就是对这个状态变量的操作,在独占模式下,假如有多个线程竞争同一个lock,那么线程一假如在tryAcquire中对state值CAS操作成功,并且将拥有线程设置为当前线程,那么线程一就拿到了锁,可以继续执行后续的同步代码,其他线程在tryAcquire对state操作失败,并且不是这个锁的拥有线程,那么这个线程将被封装成Node.Exclusive的节点放入到AQS的阻塞链表后面,等待唤醒。当持有的线程在执行完同步代码 以后,那么它将通过locksuppot.unpark唤醒其中一个阻塞的线程,这个线程再次尝试tryAcquire,如果这次尝试获取成功,那么就执行后面的同步代码,否则继续。
为什么是双向的链表,因为在多线程并发情况下,有些前任的节点的waitstatus 状态被置位CANCEL状态,那么这些节点需要被清除。
在什么情况下节点的状态被置位CANCEL,在acquireQueue方法的循环中,假设出现了某些异常,线程在final中会将该节点置位cancel
共享模式下差不多,如果线程调用tryAcquireShard方法返回false,那么当前线程会被包装成一个SHARD的node节点放入到AQS的双向链表中
AQS的head节点是一个哨兵节点,里面没有线程对象,只是一个空的节点
AQS的条件变量ConditionObject可以配合lock来实现生产者和消费者来实现线程间的同步
当在lock的同步代码里面调用条件变量的await方法,那么这个线程会被包装成CONDITION的节点放入到当前的ConditionObject的链表中,之后这个线程会释放当前的锁,,也就是操作锁对应的state变量值,并被阻塞
当有其他线程调用条件变量的signal的时候,,在内部会将条件变量头部的一个线程节点从条件链表中移除并发入到AQS的阻塞链表最后面中,然后将first节点的waitstatus置位0,,然后等待时机获取锁。
ReentranLock里面可以根据初始化参数来选择公平的同步器或者非公平的同步器,这两个同步器都是AQS的继承类,在这里,reentrantLock调用lock方法其实就是调用同步器的lock方法,而同步器的lock方法也就是调用AQS的acuqire方法
公平锁和非公平锁的区别就是,在tryAcquire的时候,公平锁首先查看AQS内部的链表里面有前节点的线程在阻塞,有的haul那么它会被放入到AQS的链表中,否则就获取设置状态值,拥有锁后执行同步代码
而非公平锁不会考虑到AQS有没有前节点线程在阻塞,他会尝试获取锁资源,成功了就执行后面的同步代码,否则被放入到AQS的阻塞链表中等待被唤醒。
为什么条件变量的await方法必须在lock方法中
是因为条件变量在await方法中首选会释放锁资源,在判断如果资源的拥有线程不是当前线程,那么代码会主动直接抛出IllegalMonitorStatementException
直接内存
也可能导致 OutOfMemoryError 异常
由于直接内存在 Java 堆外,因此它的大小不会直接受限于 -Xmx
指定的最大堆大小,但是系统内存是有限的,Java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。
缺点
- 分配回收成本较高
- 不受 JVM 内存回收管理
直接内存大小可以通过 MaxDirectMemorySize 设置
如果不指定,默认与堆的最大值 -Xmx
参数值一致
hashmap和hashtable以及concurrentHashMap 区别
hashmap和hashtable区别
1.HashMap是Hashtable的轻量级实现(非线程安全的实现)因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略,
他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable。
2.HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
3.HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
4.Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
5.最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
6.Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。
7.就HashMap与HashTable主要从三方面来说。 一.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现 二.同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的 三.值:只有HashMap可以让你将空值作为一个表的条目的key或value
hashmap和concurrentHashMap区别
一文读懂Java ConcurrentHashMap原理与实现 - 知乎
Java并发编程笔记之ConcurrentHashMap原理探究 https://www.cnblogs.com/huangjuncong/p/9478505.html
JAVA8 ConcurrentHashMap的源码完全分析 JAVA8 ConcurrentHashMap的源码完全分析_天天睡懒觉的墨鱼的博客-CSDN博客
【十四】Java集合之ConcurrentHashMap源码分析(1.8)_jy02268879的博客-CSDN博客
JDK7下ConcurrentHashMap源码分析_concurrenthashmap源码 jdk7_JeffCoding的博客-CSDN博客
java8 Java8 ConcurrentHashMap详解_hello-java-maker的博客-CSDN博客
java7中
concurrentHashMap:里面有个segment(其继承了reentrantLock),而一个Segment实例则是一个小的哈希表,每个 Segment对象就可以守护整个ConcurrentHashMap的若干个桶
每个桶是由若干个HashEntry 对象链接起来的链表,在 ConcurrentHashMap 中,不允许用 null 作为键和值。,concurrentHashMap 结构如下
通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。
ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长,但是带来的好处是写操作的时候可以只对元素所在的 Segment 进行操作即可,
java8里面的ConcurrentHashMap结构和HashMap类似
hashmap的分散和扩容机制
Java集合之HashMap详解 Java集合之HashMap详解_DivineH的博客-CSDN博客
hashmap是通过 数组+链表(拉链法) 来实现的,对初始容量(默认为: 16)和扩展因子赋值 (默认值为: 0.75 )。当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容
一个存储单元为Entry,可以发现Entry应该是一个单向链表,属性next表示后继的Entry,但是没有向前的Entry,HashMap中的节点默认都被封装成为了Node类型数据,它是HashMap对应的链表节点:将链表转为红黑树之后,节点不再以Node方式存储,而被转化为了TreeNode节点(Node链表的节点数超过了8个,则该链表会考虑转为红黑树)
初始化的时候计算capacity方法
//该方法不断地把最高位1后面的位全部变成1,然后加1,即得到了结果,那为什么要cap - 1呢?因为它要计算大于等于cap的值,如果cap为8(1000),且不减1,则计算出的结果为16(10000),为了处理这种情况,需要将cap - 1。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
hashmap的hash计算
put方法首先调用了hash方法来计算哈希值,然后通过putVal方法来添加元素。hash(Object)方法是HashMap的一个静态方法:
//若key为null,则直接返回0,否则通过h = key.hashCode()计算出key的hashcode,然后返回h ^ (h >>> 16)的值。h >>> 16为无符号向右移动16位,移位之后,h的高16位全部变成了0,计算过程如下:
//这样做的好处是,低位的信息中混入了高位的信息,这样高位的信息被变相的保留了下来,而且,HashMap已经
//使用了红黑树对table中的节点碰撞做了处理,
//因此我们只是以最简单的方式做一些移位,然后进行异或运算。采用这种计算方式,是在速度、性能、分布上做的一个平衡,掺杂的元素多了,那么生成的哈希值的随机性会增大。能使节点的分布更加均匀。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int capacity = 1 << 4;
int haseCode1 = Integer.parseInt("00011001001000010000101100000101", 2);
int haseCode2 = Integer.parseInt("00100011000010100011000001000101", 2);
int haseCode3 = Integer.parseInt("01001100001100000000011000010101", 2);
System.out.println(haseCode1 & (capacity - 1));
System.out.println(haseCode2 & (capacity - 1));
System.out.println(haseCode3 & (capacity - 1));
System.out.println("-----");
System.out.println((haseCode1 ^ (haseCode1 >>> 16)) & (capacity - 1));
System.out.println((haseCode2 ^ (haseCode2 >>> 16)) & (capacity - 1));
System.out.println((haseCode3 ^ (haseCode3 >>> 16)) & (capacity - 1));
5
5
5
-----
4
15
5
haseCode & (capacity - 1)是用来计算节点对应的数组下标,我们后面会介绍。
可以看到如果直接使用key的hashcode作为节点的哈希值,计算出来的三个几点处于同一位置,这就产生了哈希冲突,
而如果我们使用h ^ (h >>> 16)作为哈希值,计算出来的三个节点的位置都不相同,也就是说这种方式计算出的哈希值随机性更大,能使节点的分布更加均匀。
因为数组长度-1正好相当于一个“低位掩码”。这个掩码和节点的哈希值进行与操作的结果就是哈希值的高位全部归零,只保留低位值,用来做数组下标访问
// 初始化table或对table进行扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果table的原容量 > 0
if (oldCap > 0) {
// 如果原容量 >= MAXIMUM_CAPACITY,则将阈值threshold修改为Integer.MAX_VALUE,并返回原table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 计算新容量newCap = 原容量 * 2
// 若newCap < MAXIMUM_CAPACITY且旧容量oldCap >= DEFAULT_INITIAL_CAPACITY,则新的阈值newThr = 旧阈值 * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 否则,table的原容量为0,如果原阈值 > 0,则新容量newCap = oldThr
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 否则,新容量newCap = DEFAULT_INITIAL_CAPACITY(16)
// 新阈值newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值newThr为0
if (newThr == 0) {
// 计算阈值ft,ft = (float)newCap * loadFactor
float ft = (float)newCap * loadFactor;
// 根据ft来计算新阈值newThr
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新阈值
threshold = newThr;
// 创建新的哈希表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//更新table为newTab
table = newTab;
// 如果原table不为null,则需要将原table中的节点复制到新table中
if (oldTab != null) {
// 遍历原table数组,j为下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 原table的j下标存有节点,e为头结点
if ((e = oldTab[j]) != null) {
// 将原table[j]处置为null,释放空间
oldTab[j] = null;
// 如果e没有后继节点
if (e.next == null)
// 将e赋值给新table对应的首节点
newTab[e.hash & (newCap - 1)] = e;
// 如果e为红黑树节点
else if (e instanceof TreeNode)
// 重构红黑树结构到新table中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e为链表节点
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 将同一链表中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
// 若(e.hash & oldCap)为0,则该节点在新table中的下标不变
// 若(e.hash & oldCap)不为0,则该节点在新table中的下标变为j + oldCap
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新table
return newTab;
}
HashMap的数组长度要取2的整次幂。因为数组长度-1正好相当于一个“低位掩码”。这个掩码和节点的哈希值进行与操作的结果就是哈希值的高位全部归零,只保留低位值,用来做数组下标访问。同时,capacity - 1的二进制表示中的最后一位是1,这样便保证了haseCode & (capacity - 1)的最后一位可能为0,也可能为1,即计算出的下标可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,若capacity - 1的二进制表示中的最后一位是0,则计算出的下标都是偶数,所有奇数下标都没有被使用,不仅不均匀,而且还浪费了一半空间。其实,数组长度要取2的整次幂还有其他好处,
有四个节点,它们的哈希值分别为3、11、19、27,若初始容量为8,这四个节点都位于下标为3的位置,在扩容之后,哈希表容量变为16,因为3 & 8 == 0,19 & 8 == 0,所以,这两个节点仍然在新table下标为3的位置,但是11 & 8 != 0,27 & 8 != 0,所以,这两个节点会在新table下表为(3+8) = 11的位置,我们验证一下:11 & (16 - 1) = 11,27 & (16 - 1) = 11,这两个节点确实应该在下标为11的位置。这里也体现出了哈希表容量为2的整数次幂的另一个好处,在rehash时,节点在新table中的下标计算很方便。
我们还知道,当链表节点个数在插入新的节点后,如果达到转为红黑树的阈值,则需要将链表转为红黑树,将链表转化为红黑树是通过treeifyBin方法来完成的
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果table为空或者table数组太小,不满足转为红黑树的条件
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 对table数组进行扩容
resize();
// 如果符合转为红黑树的条件,且hash对应的数组位置不为null,即存在哈希值为hash的节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 遍历链表
do {
// 将Node节点转换为TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
Java string相关
String ==和equals java:String使用equals和==比较的区别_string == 跟equals_越来越好ing的博客-CSDN博客
String.intern https://www.cnblogs.com/paddix/p/5326863.html
一般用==判断对象的内存地址是否相等
结果 #1:因为str1指向的是字符串中的常量,str2是在堆中生成的对象,所以str1==str2返回false。
结果 #2:str2调用intern方法,会将str2中值(“string”)复制到常量池中,但是常量池中已经存在该字符串(即str1指向的字符串),所以直接返回该字符串的引用,因此str1==str2返回true。
#多个常量字符串进行拼接,拼接后的字符串也是存储在常量池中,故而也是常量字符串,但是如果通过stringbuffer等拼接常量字符串,那么得到的不是常量字符串(验证得到)
而String.intern 需要注意的是JDK7之前,它是将首次遇到的字符串实例复制到永久代中,而JDK7中只是记录首次出现的实例引用(即在堆中的变量地址),所以如下实例:
String s2=new StringBuffer("java").append("str").append("sm").toString();
String s3=s2.intern();
System.out.println(s2==s3);
//这个在JDK6中是false,在JDK7是true
Object.hashcode方法,equals方法
Object.hashCode
是一个native方法,
先说结论:OpenJDK8 默认hashCode的计算方法是通过和当前线程有关的一个随机数+三个确定值,运用Marsaglia's xorshift scheme随机数算法得到的一个随机数。和对象内存地址无关。
JNI验证已经能够得到很显然的结论,hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。
Object.equals方法实现就是比较==,就是比较对象内存地址是否相同
Java面试题:两个对象值相同(x.equals(y) == true),但却可有不同的hashCode,这句话对不对?
不对,如果两个对象x 和 y 满足 x.equals(y) == true,它们的哈希码(hashCode)应当相同。一般重写了equals方法就必须重写hashcode方法
ava 对于eqauls 方法和 hashCode 方法是这样规定的:
(1)如果两个对象相同(equals 方法返回 true),那么它们的hashCode 值一定要相同;
(2)如果两个对象的 hashCode 相同,它们并不一定相同。
当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
Java Object.hashCode()返回的是对象内存地址?_Arthur-Ji的博客-CSDN博客
java synchronized和偏向锁,轻量锁
https://www.cnblogs.com/paddix/p/5405678.html
1.String类为什么是不可变的?为什么是final的?
什么是不可变:定义一个字符串对象,再新建一个对象赋值给这个字符串,这个赋值过程不是修改原地址的数据,而是指向一个新的对象,这就叫不可变。如下图:
JDK源码里,String类被final修饰,String类有一个属性是char类型的数组,说明String类本质上是一个数组,这个数组也被final修饰,所以String类不可变。好处有:
- 线程安全--String对象不可变,不能被写入,所以在多线程中修改字符串的值,其他的引用指向的还是原来的对象,不会被改变。
- 用于实现StringPool--栈里面的多个引用可以指向StringPool里的同一个字符串,节省了很多堆空间,如果String是可变的,那StringPool就没有意义了。
- 便于计算哈希值-- 因为String类不可变,所以字符串很适合被hash。String类有一个hashCode()方法用于计算哈希值,在创建字符串的时候哈希值就计算出来并被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,处理速度要快过其它的键对象。
补充:
- String类中有个私有实例字段hash,表示该字符串的哈希值。
- hash值的计算过程,是依据被hash的值的特征计算的,这要求被hash的值必须固定,因此被hash的值必须不可变。
反射中,Class.forName(String name)和ClassLoader的区别?
反射:动态地获取类的内容,并映射成一个Java对象。
Java中Class.forName(String name)和ClassLoader都可用来对类进行加载,获得字节码文件。
- Class.forName(String name)将类的字节码文件加载到JVM中,内部调用的forName()方法里有一个参数是控制初始化的,是true,所以会初始化,会加载静态代码块。
- ClassLoader只是将字节码文件加载到JVM中,不会加载静态代码块。只有在用newInstance创建对象的时候才会加载静态代码块。
数据库加载驱动使用Class.forName(String name),是因为DriverManager驱动类有静态代码块。
java虚拟机的GC 垃圾收集器
首先,java8的默认使用ParallelGC收集器,也就是在新生代使用Parallel Scavenge收集器,老年代使用ParallelOld收集器
JDK 8 到底默认用的是哪款 GC 收集器? 研究了 2 天,终于知道 JDK 8 默认 GC 收集器了!_zl1zl2zl3的博客-CSDN博客
2.线程
java线程有几种创建方式,区别是什么?
首先我们来看下老师给我们讲过的创建线程的“两种”方法:
一、继承Thread类创建线程子类
1.在这子类中重写run方法,在run方法内写线程任务代码
2.创建该子类实例,即是创建了一个线程实例
3.调用该实例的start方法来启动该线程
二、建一个类去实现Runnable接口
1.该类去实现接口的run方法,run方法内写线程任务代码
2.创建该类实例,把该实例当作一个标记target传给Thread类,如:Thread t = new
Thread(该类实例);即创建一个线程对象
3.调用线程的star方法来启用该线程
扩展一下第三种老师课上没讲过的创建线程方法:
三、通过Callable接口和Future创建线程
1.创建一个类去实现Callable接口,实现该接口的call方法
CallableTest implements Callable{
public Integer call(){}
}
2.创建实现Callable接口的类的实例,用FutureTask类来包装该对象
CallableTest ct = new CallableTest();
FutureTask ft = new FutureTask(ct);
3.用FutureTask对象作为Thread对象的target创建并启动新线程
Thread t = new Thread(ft);
t.start();
优劣:
二、三两种方式是实现某接口,可以去继承其他类,操作相对灵活,并且能多个纯种共享一个对象Thread t = new
Thread(ft);里面的ft对象能多个线程共享,劣势是编程相对复杂
一种方式是继承Thread类,不能再继承其他类,编程相对简单
java线程池原理
https://www.cnblogs.com/rinack/p/9888717.html
Java线程池种类、区别和适用场景 Java线程池种类、区别和适用场景_java线程池及特性_福尔摩帅的博客-CSDN博客
java ThreadPoolExecutor源码分析 Java线程池之ThreadPoolExecutor源码分析_A__Plus的博客-CSDN博客
java内存模型相关
1主内存和工作内存 ,2内存之间的交互操作 ,3volatile的特殊规则(内存屏蔽) 4.原子性,可见性,有序性、
内存屏障的作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
Thread sleep wait yield 等方法
spring事务传播机制
1) PROPAGATION_REQUIRED ,Spring默认的事务传播级别,使⽤该级别的特点是,如果上下⽂中已经存在事务,那么就加⼊到事务中执⾏,如果当前上下⽂中不存在事务,则新建事务执⾏。所以这个级别通常能满⾜处理⼤多数的业务场景。
2)PROPAGATION_SUPPORTS ,从字⾯意思就知道,supports,⽀持,该传播级别的特点是,如果上下⽂存在事务,则⽀持事务加⼊事务,如果没有事务,则使⽤⾮事务的⽅式执⾏。所以说,并⾮所有的包在transactionTemplate.execute中的代码都会有事务⽀持。这个通常是⽤来处理那些并⾮原⼦性的⾮核⼼业务逻辑操作。应⽤场景较少。
3)PROPAGATION_MANDATORY , 该级别的事务要求上下⽂中必须要存在事务,否则就会抛出异常!配置该⽅式的传播级别是有效的控制上下⽂调⽤代码遗漏添加事务控制的保证⼿段。⽐如⼀段代码不能单独被调⽤执⾏,但是⼀旦被调⽤,就必须有事务包含的情况,就可以使⽤这个传播级别。
4)PROPAGATION_REQUIRES_NEW ,从字⾯即可知道,new,每次都要⼀个新事务,该传播级别的特点是,每次都会新建⼀个事务,并且同时将上下⽂中的事务挂起,执⾏当前新建事务完成以后,上下⽂事务恢复再执⾏。
这是⼀个很有⽤的传播级别,举⼀个应⽤场景:现在有⼀个发送100个红包的操作,在发送之前,要做⼀些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送⽇志,发送⽇志要求100%的准确,如果⽇志不准确,那么整个⽗事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的⼦事务不会直接影响到⽗事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字⾯得知,not supported ,不⽀持,当前级别的特点就是上下⽂中存在事务,则挂起事务,执⾏当前逻辑,结束后恢复上下⽂的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩⼩。我们知道⼀个事务越⼤,它存在的⻛险也就越多。所以在处理事务的过程中,要保证尽可能的缩⼩范围。举⼀个应⽤场景:⽐如⼀段代码,是每次逻辑操作都必须调⽤的,⽐如循环1000次的某个⾮核⼼业务逻辑操作。这样的代码如果包在事务中,势必造成事务太⼤,导致出现⼀些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上⽤场了。⽤当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER ,该事务更严格,上⾯⼀个事务传播级别只是不⽀持⽽已,有事务就挂起,⽽PROPAGATION_NEVER传播级别要求上下⽂中不能存在事务,⼀旦有事务,就抛出runtime异
常,强制停⽌执⾏!这个级别上辈⼦跟事务有仇。
7)PROPAGATION_NESTED ,字⾯也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下⽂中存在事务,则嵌套事务执⾏,如果不存在事务,则新建事务
3.数据结构
链表找环
找环入口
方法一:使用hashset
使用HashSet来做,遍历List的同时用Set来存储每一个节点,如果链表有环的话,一定会把相同的节点存进去,那么这就说入口。
这个方法简单粗暴,时间复杂度也不高 O(n)
方法二:使用快慢指针
从头结点出发,慢指针一次移动一个位置,快指针一次移动两个位置。如果有环的话,他们一定会在某个位置相遇。但这个位置不一定是入口,只能说在环内某个位置相遇
假设环的起点到环入口为x,而快慢指针在环k处相遇,则2(x+k)=x+m*n+k,得到x=m*n-k
相遇后,让fast从链表起点开始走,每次都是走一步,第二次相遇的节点就是环的入口
当x=10,n=17 m=1,k=7,相遇后fast走10步时,此时slow走到了起点,因为10+7=17正好
当x=17,n=10 m=2,k=3 相遇后fast 走17步,slow则是17+3=20正好也走到了起点
算法如下:
public static ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode fast = pHead;
ListNode slow = pHead;
while(fast != null && fast.next !=null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow)
break;
}
if(fast == null || fast.next == null)
return null;
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
动态规划
深度好文:动态规划详解 https://www.cnblogs.com/labuladong/p/12320371.html
动态规划三要素:重复子问题,最优子结构,状态转移方程
关键:dp table和自底向上
广度优先搜索和深度优先搜索,Dijkstra 算法
图和二叉树一般用DFS,BFS算法用的比较多
图的广度优先搜索(BFS)和深度优先搜索(DFS)算法解析 图的广度优先搜索(BFS)和深度优先搜索(DFS)算法解析_bfs算法_Chida15的博客-CSDN博客
BFS:visted数组表示访问的状态,队列处理当前节点接触的每个节点
DFS:visted数组,
Dijkstra 算法,又叫迪科斯彻算法(Dijkstra),算法解决的是有向图中单个源点到其他顶点的最短路径问题
排序算法
快速排序:
希尔排序:
堆排序:
直接排序:
求数组中最大k个值
用最小堆,求最小k个值,可以使用最大堆
如何从100万个数中找出最大的前100个数
算法如下:根据快速排序划分的思想
(1) 递归对所有数据分成[a,b)b(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数
(2) 对(b,d]重复(1)操作,直到最右边的区间个数小于100个。注意[a,b)区间不用划分
(3) 返回上一个区间,并返回此区间的数字数目。接着方法仍然是对上一区间的左边进行划分,分为[a2,b2)b2(b2,d2]两个区间,取(b2,d2]区间。如果个数不够,继续(3)操作,如果个数超过100的就重复1操作,直到最后右边只有100个数为止。
2.先取出前100个数,维护一个100个数的最小堆,遍历一遍剩余的元素,在此过程中维护堆就可以了。具体步骤如下:
step1:取前m个元素(例如m=100),建立一个小顶堆。保持一个小顶堆得性质的步骤,运行时间为O(lgm);建立一个小顶堆运行时间为m*O(lgm)=O(m lgm);
step2:顺序读取后续元素,直到结束。每次读取一个元素,如果该元素比堆顶元素小,直接丢弃
如果大于堆顶元素,则用该元素替换堆顶元素,然后保持最小堆性质。最坏情况是每次都需要替换掉堆顶的最小元素,因此需要维护堆的代价为(N-m)*O(lgm);
最后这个堆中的元素就是前最大的10W个。时间复杂度为O(N lgm)。
补充:这个方法的说法也可以更简化一些:
假设数组arr保存100个数字,首先取前100个数字放入数组arr,对于第101个数字k,如果k大于arr中的最小数,则用k替换最小数,对剩下的数字都进行这种处理。
3.分块查找
先把100w个数分成100份,每份1w个数。先分别找出每1w个数里面的最大的数,然后比较。找出100个最大的数中的最大的数和最小的数,取最大数的这组的第二大的数,与最小的数比较。。。。
10亿个数和10万个数,如何求交集
小数组hash,然后遍历大数组即可。
红黑树和自平衡树,二三查找树
平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),
1.红黑树和自平衡二叉(查找)树区别 1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。 2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。 AVL树是最早出现的自平衡二叉(查找)树
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
红黑树和AVL树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。 红黑树是牺牲了严格的高度平衡的优越条件为代价红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。 此外,由于它的设计,任何不平衡都会在三次旋转之内解决。 当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。 红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高.
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
avl树平衡算法这篇算法讲的很好 详细图文——AVL树_带翅膀的猫的博客-CSDN博客
红黑树(一)之 原理和算法详细介绍 https://www.cnblogs.com/skywang12345/p/3245399.html
关于红黑树(R-B tree)原理,看这篇如何 ☆☆ https://www.cnblogs.com/LiaHon/p/11203229.html
链表交叉问题
https://www.cnblogs.com/qingergege/p/7825936.html
1.两个单链表交叉只能是Y型,所以可以通过判断最后一个节点是否为同一个节点来判断是否交叉
bool IsCross(Node *head1, Node *head2)
{
if (!head1 || !head2) {
return false;
}
Node *p1 = head1;
Node *p2 = head2;
while (p1->next) {
p1 = p1->next;
}
while (p2->next) {
p2 = p2->next;
}
return (p1 == p2);
}
2两个单链表交叉Y型,遍历两个链表,记录长度分别为L1和L2,先让长的链表向后移动abs(L1-L2),然后在逐个比较结点,第一个相等的结点即为交点。
Node *FindCross(Node *head1, Node *head2)
{
if (!head1 || !head2) {
return NULL;
}
/* 求出两个链表的长度 */
Node *p1, *p2;
p1 = head1;
p2 = head2;
int len1, len2, len;
len1 = len2 = 0;
while (p1) {
len1++;
p1 = p1->next;
}
while (p2) {
len2++;
p2 = p2->next;
}
/* 将长链表先移动len个结点 */
int i;
len = abs(len1 - len2);
p1 = head1;
p2 = head2;
if (len1 > len2) {
for (i = 0; i < len; ++i) {
p1 = p1->next;
}
} else {
for (i = 0; i < len; ++i) {
p2 = p2->next;
}
}
/* 遍历 找到第一个相等的结点即为交点 */
while (!p1) {
p1 = p1->next;
p2 = p2->next;
if (p1 == p2) {
return p1;
}
}
return NULL;
}
3.如果链表都有环,则只肯能有下面两种情况(如下图)。两种情况区分的方法为:入环点是否相同。
如果相同则为第一种情况:那么查找第一个相交点与无环的单链表相交找第一个交点的方法一样。
如果入环点不同,则为第二种情况,这个相交点或者为list1 的入环点loop1或者为list2的入环点loop2。
情况1实例数据(List1:1->2->3->4->5->6->7->4,List2:0->9->8->2->3->4->5->6->7->4,第一个交点为2)
情况2实例数据(List1:1->2->3->4->5->6->7->4,List2:0->9->8->6->7->4->5->6,第一个交点为4或6)