java高级面试题第一版

1、数据库四个特性是什么,事务传播性是怎么样的?spring事务和数据库事务的区别关系?

原子性:强调事务的原子行为,要么全部成功,要么全部回滚

一致性:事务执行的结果必须从数据库一个一致性状态变更到另一个一致性状态

隔离性:一个事务执行过程中不能被其他事务干扰

持久性:一个事务提交后,它对数据库中的数据的改变是永久性的不能回滚

事务隔离级别:

读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)、串行化(SERIALIZABLE)

事务七种传播性:

事务传播行为类型 说明

PROPAGATION_REQUIRED :如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS :支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY :使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW :新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED :以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER :以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED :如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

2、HashMap/concurrentHashMap区别和底层实现、TreeMap特点

(1)concurrentHashMap:

底层采用分段的数组+链表实现,线程安全

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁

扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

(2)TreeMap:

连接:https://www.cnblogs.com/LiaHon/p/11221634.html

TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;

TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;

TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;

TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;

3、Synchronized的四大用法和区别

参考连接:https://www.jianshu.com/p/aa6d2d4d0cb7

(1)Synchronized修饰普通方法的作用与含义

作用:Synchronized修饰普通方法就是控制多个线程访问同一个变量对应 Synchronized 修饰的方法时候达到同步效果;特别注意是同一个变量,如果多个线程访问非同一个变量对应 Synchronized 修饰的方法时候是异步执行的,无法达到同步效果。

含义:变量锁,只针对同个变量生效

(2)Synchronized修饰静态方法作用与含义

作用:Synchronized修饰静态方法就是控制多个线程访问同一类对应 Synchronized 修饰的静态方法时候达到同步效果;无论是否为同一变量,都能达到同步的效果。

含义:类锁,只要是访问同个类 Synchronized修饰静态方法就会形成同步效果

(3)Synchronized(this) 作用与含义

该作用与含义和Synchronized修饰普通方法是一模一样的

(4)Synchronized(xxxx.class) 作用与含义

该作用与含义和Synchronized修饰静态方法是一模一样的

4、分布式事务

在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务。

 

5、synchronized 和 Lock、ReentrantLock有什么区别

synchronized 与Lock的区别

synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁,所以常在finally代码块释放。

通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

主要区别如下:

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作。

ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁。

ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

6、RocketMQ的高可用机制

master slave 配合,master 支持读、写,slave 只读,producer 只能和 master 连接写入消息,consumer 可以连接 master 和 slave。

consumer消费者

当 master 不可用或者繁忙时,consumer 会被自动切换到 slave 读。所以,即使 master 出现故障,consumer 仍然可以从 slave 读消息,不受影响

producer提供者

创建 topic 时,把 message queue 创建在多个 broker 组上(brokerName 一样,brokerId 不同),当一个 broker 组的 master 不可用后,其他组的 master 仍然可以用,producer 可以继续发消息。

备注broker含义:

broker是rocketmq的核心,borker做了绝大部分核心工作,包括接受请求、处理消费、消息持久、服务端消息过滤以及内部服务HA集群一致性和构建消息序列化文件的索引等。

连接:https://blog.csdn.net/li_xiao_dai/article/details/79917875

7、RocketMQ如何解决重复消费问题?重复消费保证幂等性

1、校验数据 在数据库中是否存在

2、发送消息时,生成唯一ID存入redis,并且放入消息发送队列内,在消费时判断唯一ID的是否被消费过,消费过则不再消费。 建议采用此种方式,通过redis单线程特点来实现幂等性校验

(比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

利弊分析:此方法会造成redis内存储大量无用ID,定期清除redis内的唯一建值,可以在本次订单或者事务完成时,删除对应的redis唯一建,避免redis内,存储量过大。

设置过期时间方式不推荐,消费没有真正成功,不在redis内存储值)

3、通过数据库唯一索引,解决消息重复消费,但是不影响数据库数据,不会出现重复数据。

(此处方法,解决最后一步问题,建议存在上一步redis幂等校验,也保留此处数据唯一索引,校验方式,保证最后一道屏障不出现问题)

4、原理就是利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。(此方法弊端特别大,由于消费时,处于无序并发消费,一台服务器投递了两条消息过来处理,数据库不具备串行化隔离级别能力,故而需要外层代码加锁实现,从而大大降低了,消费端的处理性能。需要增加分布式锁,破坏分布式高吞吐性能,此方式不考虑)

8、HashSet 和 HashMap 区别

hashSet实现了set接口,hashSet底层实现了hashMap的大部分方法。

(1)hashMap存储对象过程:

1、对HahMap的Key调用hashCode()方法,返回int值,即对应的hashCode;

2、把此hashCode作为哈希表的索引,查找哈希表的相应位置,若当前位置内容为NULL,则把hashMap的Key、Value包装成Entry数组,放入当前位置;

3、若当前位置内容不为空,则继续查找当前索引处存放的链表,利用equals方法,找到Key相同的Entry数组,则用当前Value去替换旧的Value;

4、若未找到与当前Key值相同的对象,则把当前位置的链表后移(Entry数组持有一个指向下一个元素的引用),把新的Entry数组放到链表表头;

JDK1.7 因为是头插法,可能会造成循环链表

JDK1.8 是尾插法

源码分析链接:https://blog.csdn.net/AJ1101/article/details/79413939

源码解读:

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

hashmap的put值方式,实际调用底层封装putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

// 如果存储元素的table为空,则进行必要字段的初始化

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

//当前结点为null则新增一个结点,存放当前存入值

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

Node<K,V> e; K k;

//判断hash是否相等并且key值完全一致,如果是obj则equals相等,则替换当前值

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

//判断当前结点是否为红黑树,是红黑树则继续插入值

else if (p instanceof TreeNode)

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

else {

//非红黑树并且hash或key不相等,则遍历当前链表,链表长度小于8,则插入新的结点在链表上

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

//如果长度大于8时,则转化为红黑树结构

treeifyBin(tab, hash);

break;

}

//判断链表已存在结点,是否与新增结点hash和key相等,如果相等则跳出循环。

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

//遍历下一个结点

p = e;

}

}

// 如果存在这个映射就覆盖

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

//覆盖旧值后将旧值返回

return oldValue;

}

}

//e为空,则判定为新增了链表结点或增加了红黑树高度,此时需判断是否达到扩容临界值。

++modCount;

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

resize源码分析:

final Node<K,V>[] resize() {

Node<K,V>[] oldTab = table;

int oldCap = (oldTab == null) ? 0 : oldTab.length;

int oldThr = threshold;

int newCap, newThr = 0;

if (oldCap > 0) {

//探究hashmap扩容临界值,只能为1>>30,实际为2^30次方,为什么不能为1>>31?

由于二进制最高位代表01分别代表正负之分,故而不能左移动31位,只能是30位。

那为什么不能左移32位为1>>32 由于int整数型 最高只能存储32位二进制数,超出则不能存储。

故而下方代码,设置当前扩容值为当前最大值Integer.MAX_VALUE

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

//使用newCap判断假如扩容一倍,是否超过限定最大扩容值。并且当前map长度大于或者正好等于16,才进行扩大一倍操作。

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

newThr = oldThr << 1; // double threshold

}

//当原始链表长度大于0时,新表扩容值为旧表当前链表长度

else if (oldThr > 0) // initial capacity was placed in threshold

newCap = oldThr;

else { // zero initial threshold signifies using defaults

//假如当前hashmap为新new Map集合则设置默认值

newCap = DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

if (newThr == 0) {

float ft = (float)newCap * loadFactor;

newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

}

threshold = newThr;// 更新填充因子

//忽略警告信息两种级别警告,后续可以细节研究

@SuppressWarnings({"rawtypes","unchecked"})

//创建新的node数据,此处也可以看出,hashmap为懒加载机制,在第一次put的时候才会去,初始化散列表。

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

// 调整数组大小之后,需要调整红黑树或者链表的指向

if (oldTab != null) {

for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

//赋值并且判断遍历结点不为空

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

//只存在一个头结点则替换当前旧值至新扩容对象内

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

//是否为红黑树结构,调整红黑树的结构

else if (e instanceof TreeNode)

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

//若为链表,则调整当前链表

else { // preserve order

Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

do {

next = e.next;

//重新计算链表hash位置

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;

}

}

}

}

}

return newTab;

}

 

以链表存储还是按红黑树结构存储treeifyBin源码分析

final void treeifyBin(Node<K,V>[] tab, int hash) {

int n, index; Node<K,V> e;

// 如果元素总个数小于64,则继续进行扩容,结点指向调节,此处说明假如链表长度超过了8,但是当前数组长度小于64,仍然不会转换为红黑树。

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

resize();

// 先找到那个链表的头

else if ((e = tab[index = (n - 1) & hash]) != null) {

TreeNode<K,V> hd = null, tl = null;

do {

//创建红黑树根结点

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);

}

}

 

链表转红黑树源码分析

final void treeify(Node<K,V>[] tab) {

TreeNode<K,V> root = null;

// TreeNode<K,V> x = this  相当于初始化了一个结点

for (TreeNode<K,V> x = this, next; x != null; x = next) {

next = (TreeNode<K,V>)x.next;

x.left = x.right = null;

if (root == null) {

x.parent = null;

x.red = false;

root = x;

}

else {

K k = x.key;

int h = x.hash;

Class<?> kc = null;

for (TreeNode<K,V> p = root;;) {

int dir, ph;

K pk = p.key;

if ((ph = p.hash) > h)

dir = -1;

else if (ph < h)

dir = 1;

else if ((kc == null &&

(kc = comparableClassFor(k)) == null) ||

(dir = compareComparables(kc, k, pk)) == 0)

dir = tieBreakOrder(k, pk);

 

TreeNode<K,V> xp = p;

if ((p = (dir <= 0) ? p.left : p.right) == null) {

x.parent = xp;

if (dir <= 0)

xp.left = x;

else

xp.right = x;

//此处为红黑树平衡器,调整节点分布平衡方法

root = balanceInsertion(root, x);

break;

}

}

}

}

moveRootToFront(tab, root);

}

(2)hashSet存储对象过程:

往HashSet添加元素的时候,HashSet会先调用元素的hashCode方法得到元素的哈希值 ,

然后通过元素 的哈希值经过移位等运算,就可以算出该元素在哈希表中 的存储位置。

情况1: 如果算出元素存储的位置目前没有任何元素存储,那么该元素可以直接存储到该位置上。

情况2: 如果算出该元素的存储位置目前已经存在有其他的元素了,那么会调用该元素的equals方法与该位置的元素再比较一次

,如果equals返回的是true,那么该元素与这个位置上的元素就视为重复元素,不允许添加,如果equals方法返回的是false,那么该元素运行添加。

源码分析:

HashSet初始化过程:

hashSet实现依托于hashmap.

public HashSet() {

map = new HashMap<>();

}

 

//value键内放值静态私有变量,使用key不唯一特性,保证hashSet结构特性,也可以称为特性版hashmap。add方法实则为包装了hashmap的put方法。

private static final Object PRESENT = new Object();

public boolean add(E e) {

return map.put(e, PRESENT)==null;

}

区别:

 

9、Object的常见方法总结

简介:toString()、wait()、notify()、notifyAll()、getClass()、hashCode()、equals()、clone()、finalize()。

wait包含三种方法:

1、public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 2、public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 3、public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。

finalize方法含义:

1、protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

10、hashCode()与equals()的相关规定

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,对两个对象分别调用equals方法都返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

11、为什么两个对象有相同的hashcode值,它们也不一定是相等的?

因为hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。

我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

12、==与equals

== : 它的作用是判断两个对象的地址是不是相等。

即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。

情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。

备注:String类重写了equals和hashCode方法,从而具备判断在堆中的两个字段串是否真正相等。

源码:

 

13、ConcurrentHashMap 和 Hashtable 的区别

1、底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

2、实现线程安全的方式(重要):

 (1) 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;疑问:细致学习jdk1.8的concurrentHahshMap实现

(2) Hashtable(同一把锁):使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

14、ConcurrentHashMap线程安全的具体实现方式/底层具体实现

(1)JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable { }

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

(2)JDK1.8(上面有示意图)

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

源码分析链接:https://blog.csdn.net/u010235716/article/details/90237903

源码分析:

putVal源码写入分析

final V putVal(K key, V value, boolean onlyIfAbsent) {

//首先concurrentHashMap不支持控制写入,抛出异常

if (key == null || value == null) throw new NullPointerException();

//计算当前hash值

int hash = spread(key.hashCode());

int binCount = 0;

for (Node<K,V>[] tab = table;;) {

Node<K,V> f; int n, i, fh;

//初始化当前数组,如果当前数组为空

if (tab == null || (n = tab.length) == 0)

//同样为懒加载方式,初始化当前数组对象

tab = initTable();

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

// 如果下标返回的节点为空,则通过cas操作将新的值封装成node插入即可,如果cas失败,说明存在竞争,则进入下一次循环

if (casTabAt(tab, i, null,

new Node<K,V>(hash, key, value, null)))

break; // no lock when adding to empty bin

}

else if ((fh = f.hash) == MOVED)

tab = helpTransfer(tab, f);

else {

V oldVal = null;

synchronized (f) {

//tabAt寻找指定数组在内存中i位置的数据,

if (tabAt(tab, i) == f) {

if (fh >= 0) {

binCount = 1;

for (Node<K,V> e = f;; ++binCount) {

K ek;

//判定hash一致,并且建值相等,则覆盖原有值

if (e.hash == hash &&

((ek = e.key) == key ||

(ek != null && key.equals(ek)))) {

oldVal = e.val;

if (!onlyIfAbsent)

e.val = value;

break;

}

Node<K,V> pred = e;

//判断链表下一个位置是否为空,如果为空则填入对应值

if ((e = e.next) == null) {

pred.next = new Node<K,V>(hash, key,

value, null);

break;

}

}

}

//对象为红黑树结构,则将数据插入至红黑树

else if (f instanceof TreeBin) {

Node<K,V> p;

binCount = 2;

if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

value)) != null) {

oldVal = p.val;

if (!onlyIfAbsent)

p.val = value;

}

}

}

}

if (binCount != 0) {

// 当链表长度 大于等于 8 之后 判断是否进行链转红黑树

if (binCount >= TREEIFY_THRESHOLD)

treeifyBin(tab, i);

if (oldVal != null)

return oldVal;

break;

}

}

}

//计数方法

addCount(1L, binCount);

return null;

}

 

initTable方法源码分析:

private final Node<K,V>[] initTable() {

Node<K,V>[] tab; int sc;

while ((tab = table) == null || tab.length == 0) {

//其他线程已经设置了sizeCtl为-1,故而不需要再初始化

if ((sc = sizeCtl) < 0)

Thread.yield(); // lost initialization race; just spin

// CAS,将 sizeCtl 设置为 -1,代表抢到了锁

else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

try {

if ((tab = table) == null || tab.length == 0) {

int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

//无警告,新增数组结构

@SuppressWarnings("unchecked")

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

table = tab = nt;

//无符号右移2位,无符号为无论正负数左移最高位均补0

sc = n - (n >>> 2);

}

} finally {

//此处将数组长度赋值于sizeCtl上

sizeCtl = sc;

}

break;

}

}

return tab;

}

 

treeifyBin源码分析:

private final void treeifyBin(Node<K,V>[] tab, int index) {

Node<K,V> b; int n, sc;

if (tab != null) {

//与hashmap有些类似,数组长度不大于64,无法转换红黑树,只能继续出发扩容。

if ((n = tab.length) < MIN_TREEIFY_CAPACITY)

tryPresize(n << 1);

else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {

synchronized (b) {

if (tabAt(tab, index) == b) {

TreeNode<K,V> hd = null, tl = null;

//遍历当前位置链表,存入红黑树结构内。

for (Node<K,V> e = b; e != null; e = e.next) {

TreeNode<K,V> p =

new TreeNode<K,V>(e.hash, e.key, e.val,

null, null);

if ((p.prev = tl) == null)

hd = p;

else

tl.next = p;

tl = p;

}

setTabAt(tab, index, new TreeBin<K,V>(hd));

}

}

}

}

}

 

tryPresize源码分析:

private final void tryPresize(int size) {

// c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。

int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :

tableSizeFor(size + (size >>> 1) + 1);

int sc;

while ((sc = sizeCtl) >= 0) {

Node<K,V>[] tab = table; int n;

if (tab == null || (n = tab.length) == 0) {

n = (sc > c) ? sc : c;

//CAS获得初始化锁

if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

try {

if (table == tab) {

@SuppressWarnings("unchecked")

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

table = nt;

sc = n - (n >>> 2);

}

} finally {

sizeCtl = sc;

}

}

}

else if (c <= sc || n >= MAXIMUM_CAPACITY)

break;

//假如当前值数组与变量table一致,则开始扩容操作

else if (tab == table) {

int rs = resizeStamp(n);

if (sc < 0) {

Node<K,V>[] nt;

if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||

sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||

transferIndex <= 0)

break;

//获得cas锁执行数据迁移操作

if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))

transfer(tab, nt);

}

//获得cas锁执行数据迁移操作,并且设置sc为负数,在下次循环执行上步if(sc<0)操作

else if (U.compareAndSwapInt(this, SIZECTL, sc,

(rs << RESIZE_STAMP_SHIFT) + 2))

transfer(tab, null);

}

}

}

 

transfer源码分析:

源码参考链接:https://blog.csdn.net/sihai12345/article/details/79383766

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

int n = tab.length, stride;

// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16 // stride 可以理解为”步长“,有 n 个位置是需要进行迁移的, // 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务

if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)

stride = MIN_TRANSFER_STRIDE; // subdivide range

// 如果 nextTab 为 null,先进行一次初始化 // 前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null // 之后参与迁移的线程调用此方法时,nextTab 不会为 null

if (nextTab == null) { // initiating

try {

@SuppressWarnings("unchecked")

// 容量翻倍

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];

nextTab = nt;

} catch (Throwable ex) { // try to cope with OOME

sizeCtl = Integer.MAX_VALUE;

return;

}

// nextTable 是 ConcurrentHashMap 中的属性

nextTable = nextTab;

// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置

transferIndex = n;

}

int nextn = nextTab.length;

// ForwardingNode 翻译过来就是正在被迁移的 Node // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后, // 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了 // 所以它其实相当于是一个标志。

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了

boolean advance = true;

boolean finishing = false; // to ensure sweep before committing nextTab

for (int i = 0, bound = 0;;) {

Node<K,V> f; int fh;

while (advance) {

int nextIndex, nextBound;

if (--i >= bound || finishing)

advance = false;

else if ((nextIndex = transferIndex) <= 0) {

i = -1;

advance = false;

}

else if (U.compareAndSwapInt

(this, TRANSFERINDEX, nextIndex,

nextBound = (nextIndex > stride ?

nextIndex - stride : 0))) {

bound = nextBound;

i = nextIndex - 1;

advance = false;

}

}

if (i < 0 || i >= n || i + n >= nextn) {

int sc;

if (finishing) {

nextTable = null;

table = nextTab;

sizeCtl = (n << 1) - (n >>> 1);

return;

}

if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {

if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

return;

finishing = advance = true;

i = n; // recheck before commit

}

}

else if ((f = tabAt(tab, i)) == null)

advance = casTabAt(tab, i, null, fwd);

else if ((fh = f.hash) == MOVED)

advance = true; // already processed

else {

synchronized (f) {

if (tabAt(tab, i) == f) {

Node<K,V> ln, hn;

if (fh >= 0) {

int runBit = fh & n;

Node<K,V> lastRun = f;

for (Node<K,V> p = f.next; p != null; p = p.next) {

int b = p.hash & n;

if (b != runBit) {

runBit = b;

lastRun = p;

}

}

if (runBit == 0) {

ln = lastRun;

hn = null;

}

else {

hn = lastRun;

ln = null;

}

for (Node<K,V> p = f; p != lastRun; p = p.next) {

int ph = p.hash; K pk = p.key; V pv = p.val;

if ((ph & n) == 0)

ln = new Node<K,V>(ph, pk, pv, ln);

else

hn = new Node<K,V>(ph, pk, pv, hn);

}

setTabAt(nextTab, i, ln);

setTabAt(nextTab, i + n, hn);

setTabAt(tab, i, fwd);

advance = true;

}

else if (f instanceof TreeBin) {

TreeBin<K,V> t = (TreeBin<K,V>)f;

TreeNode<K,V> lo = null, loTail = null;

TreeNode<K,V> hi = null, hiTail = null;

int lc = 0, hc = 0;

for (Node<K,V> e = t.first; e != null; e = e.next) {

int h = e.hash;

TreeNode<K,V> p = new TreeNode<K,V>

(h, e.key, e.val, null, null);

if ((h & n) == 0) {

if ((p.prev = loTail) == null)

lo = p;

else

loTail.next = p;

loTail = p;

++lc;

}

else {

if ((p.prev = hiTail) == null)

hi = p;

else

hiTail.next = p;

hiTail = p;

++hc;

}

}

ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :

(hc != 0) ? new TreeBin<K,V>(lo) : t;

hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :

(lc != 0) ? new TreeBin<K,V>(hi) : t;

setTabAt(nextTab, i, ln);

setTabAt(nextTab, i + n, hn);

setTabAt(tab, i, fwd);

advance = true;

}

}

}

}

}

}

15、synchronized 和 ReentrantLock 的区别

(1)两者都是可重入锁

“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

(2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

(3)ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

1)ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

2)ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

3)synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

(4)两者的性能已经相差无几

在JDK1.6之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。

JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReentrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReentrantLock一样,在很多地方都是用到了CAS操作。

16、为什么要用线程池

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

17、简单介绍一下Nginx

Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。

  • 正向代理:某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的ip和端口号。正向代理比较常见的一个例子就是 VPN 了。
  • 反向代理: 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。

(1)负载均衡

在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器

的压力,多台服务器(集群)共同完成工作任务,从而提高了数据的吞吐量。

Nginx支持的weight轮询(默认)、ip_hash、fair、url_hash这四种负载均衡调度算法,感兴

趣的可以自行查阅。

负载均衡相比于反向代理更侧重的是将请求分担到多台服务器上去,所以谈论负载均衡只有

在提供某服务的服务器大于两台时才有意义。

(2)动静分离

动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。

18、为什么要用 Nginx

Nginx 有以下5个优点:

  1. 高并发、高性能(这是其他web服务器不具有的)
  2. 可扩展性好(模块化设计,第三方插件生态圈丰富)
  3. 高可靠性(可以在服务器行持续不间断的运行数年)
  4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx服务的情况下升级 Nginx)
  5. BSD许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本)

19、Nginx 的四个主要组成部分了解吗

  • Nginx 二进制可执行文件:由各模块源码编译出一个文件
  • nginx.conf 配置文件:控制Nginx 行为
  • acess.log 访问日志: 记录每一条HTTP请求信息
  • error.log 错误日志:定位问题

20、LinkedHashMap了解基本原理、哪两种有序、如何用它实现LRU

LRU:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

HashMap 有一个不足之处就是在迭代元素时与插入顺序不一致。而大多数人都喜欢按顺序做某些事情,所以,LinkedHashMap 就是针对这一点对 HashMap 进行扩展,主要新增了**「两种迭代方式」**:

  • 按插入顺序 - 保证迭代元素的顺序与插入顺序一致
  • 按访问顺序 - 一种特殊的迭代顺序,从最近最少访问到最多访问的元素访问顺序,非常适合构建 LRU 缓存

linkedHashMap 设置按访问顺序排列,判断当前访问元素不是尾结点则将它与尾结点交换,元素再插入后,将会删除元素最旧,访问次数最少的元素,在其中相当于是头节点。

  • afterNodeAccess 的原理是:访问的元素如果不是尾节点,那么就把它与尾节点交换,所以随着元素的访问,访问次数越多的元素越靠后
  • afterNodeRemoval 这个没有特殊操作,正常的断开链条
  • afterNodeInsertion 的原理是:元素插入后,可能会删除最旧的、访问次数最少的元素,也就是头节点

是否需要删除头结点,需要afterNodeInsertion方法中removeEldestEntry()方法决定。

源码分析:

 

21、TreeMap:了解数据结构、了解其key对象为什么必须要实现Compare接口、如何用它实现一致性哈希

key实现Compare接口,是为了实现自动排序

treeMap实现一致性hash,需要借助一个hash散列算法,保证它的散列范围。

treeMap的一致性hash未能真正了解,看一下sync锁的实际用法,以及在concurrentHashMap内的应用。

22、spring bean的生命周期

实例化bean对象(属性注入和构造函数注入)

设置对象属性(依赖注入)

如果bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。

如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身

将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法

调用Bean的初始化方法

将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法

使用Bean

容器关闭之前,调用Bean的销毁方法

 

属性注入和构造函数注入哪个会带来循环依赖问题?

23、java SPI

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制

24、java threadLocal的使用场景

ThreadLocal 是 JDK java.lang 包中的一个用来实现相同线程数据共享不同的线程数据隔离的一个工具 需细看 原理模糊 未完全理解

 

25、服务器IO瓶颈对MySQL性能的影响

https://www.cnblogs.com/wangdong/p/9913444.html

 

26、Zookeeper 怎么保证分布式事务的最终一致性?

https://blog.csdn.net/youanyyou/article/details/113577302

 

27、mysql工作原理

https://www.cnblogs.com/albertzhangyu/p/9696479.html

 

 

28、spring原理细致了解

 

29、redis lettuce 伴随超时问题 慎用。

jedis pool又是线程不安全的

https://blog.csdn.net/y666666y/article/details/109465754

负载均衡器 TCP 重置和空闲超时

因为通过用Wireshark发现jedis默认是发心跳包的,而lettuce是不发心跳包的

https://docs.microsoft.com/zh-cn/azure/load-balancer/load-balancer-tcp-reset

 

https://blog.csdn.net/weixin_39835965/article/details/111296032

 

30、Wireshark分析工具学会使用

 

31、缓存击穿,缓存雪崩、缓存穿透

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值