网络安全最全ConcurrentHashMap1-8源码解读及如何保证线程安全(2),2024网络安全面经

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

默认sizeCtl = 16,初始化时可以进行设置

基本属性

/**

  • 最大的可能的表容量。该值必须为正好1<<30保持在Java数组分配和索引
  • 两个表大小的权限,进一步需要因为32位哈希字段的前两位用于控制目的。
    */
    //最大的容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;

/**

  • 默认初始容量
    */
    private static final int DEFAULT_CAPACITY = 16;

/**

*/
//最大数组大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**

  • 默认的并发度
    */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**

  • 负载因子大小
    */
    private static final float LOAD_FACTOR = 0.75f;

/**

  • 对象使用树而不是列表时的bin计数阈值本。当向a添加一个元素时,箱子被转换为树至少有这么多节点的bin。这个值必须更大
  • 大于2,且应至少为8,以符合假设删除关于转换回普通箱子的树收缩。
    */
    //树化的临界值
    static final int TREEIFY_THRESHOLD = 8;

/**

*/
//解除树化的值
static final int UNTREEIFY_THRESHOLD = 6;

/**

  • 容器可能被treeified的最小表容量。(否则,如果容器中有太多节点,将调整表的大小。)
  • 需要避免的值至少为4 * TREEIFY_THRESHOLD调整大小和treeification阈值之间的冲突。
    */
    //树化的最小容器数量
    static final int MIN_TREEIFY_CAPACITY = 64;

/**

  • 每个传送步骤的最小还原次数。范围是细分以允许多个大小调整线程。
  • 这个值作为一个下界,以避免遇到大小调整器过度的内存争用。值应该是至少DEFAULT_CAPACITY。
    */
    private static final int MIN_TRANSFER_STRIDE = 16;

/**

  • 用于生成戳记的位数,单位为sizeCtl。对于32位数组必须至少为6。
    */
    private static int RESIZE_STAMP_BITS = 16;

/**

  • 可以帮助调整大小的最大线程数。必须适合32 - RESIZE_STAMP_BITS位。
    */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**

  • 记录尺寸戳记的位位移,单位为sizeCtl。
    */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

可以看出相比1.7,增加了树化的相关临界属性,去除了Segment的相关属性

构造方法

无参构造方法

源码

public ConcurrentHashMap() {
}

默认大小为16

传入初始化大小的构造方法

为负数是抛出异常

public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}

动态调整sizeCtl

调用tableSizeFor方法

保证sizeCtl必须是2的n次方

传入一个map的构造方法

sizeCtl初始化也是16,然后将map中的元素全部put进ConcurrentHashMap

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}

传入初始化容量和加载因子

默认并发度为1

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}

传入初始容量,负载因子和并发度

public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}

输入的initialCapacity的大小来确定一个最小的且大于等于initialCapacity大小的2的n次幂,

如initialCapacity为15,则sizeCtl为16,若initialCapacity为16,则sizeCtl为16。

若initialCapacity大小超过了允许的最大值,则sizeCtl为最大值。

值得注意的是,构造函数中的concurrencyLevel参数已经在JDK1.8中的意义发生了很大的变化,其并不代表所允许的并发数,

其只是用来确定sizeCtl大小,在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。

Node介绍

源码

static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的hash值
final K key; //key
volatile V val; //value
volatile Node<K,V> next; //表示链表中的下一个节点

Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
}

这是1.8中构成每个元素的基本类

TreeNode

构造树的节点

static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;

TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
}

TreeBin介绍

源码:

static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock //表示持有写锁
static final int WAITER = 2; // set when waiting for write lock //表示在等待写锁
static final int READER = 4; // increment value for setting read lock//表示正在设置读说
}

用作树的头结点,只存储root和first节点,不存储节点的key、value值。

ForwardingNode

在转移的时候放在头部的节点,是一个空节点

static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}

常用方法

put

调用putVal方法,并且putVal的第三个参数设置为false

当设置为false的时候表示这个value一定会设置

true的时候,只有当这个key的value为空的时候才会设置

public V put(K key, V value) {
return putVal(key, value, false);
}

putVal方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // 键或值为空,抛出异常
// 键的hash值经过计算获得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) // 表为空或者表的长度为0
// 初始化表
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不为空并且表的长度大于0,并且该桶不为空
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) // 比较并且交换值,如tab的第i项为空则用新生成的node替换
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 该结点的hash值为MOVED
// 进行结点的转移(在扩容的过程中)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 加锁同步
if (tabAt(tab, i) == f) { // 找到table表下标为i的节点
if (fh >= 0) { // 该table表中该结点的hash值大于0
// binCount赋值为1
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 无限循环
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // 结点的hash值相等并且key也相等
// 保存该结点的val值
oldVal = e.val;
if (!onlyIfAbsent) // 进行判断
// 将指定的value保存至结点,即进行了结点值的更新
e.val = value;
break;
}
// 保存当前结点
Node<K,V> pred = e;
if ((e = e.next) == null) { // 当前结点的下一个结点为空,即为最后一个结点
// 新生一个结点并且赋值给next域
pred.next = new Node<K,V>(hash, key,
value, null);
// 退出循环
break;
}
}
}
else if (f instanceof TreeBin) { // 结点为红黑树结点类型
Node<K,V> p;
// binCount赋值为2
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) { // 将hash、key、value放入红黑树
// 保存结点的val
oldVal = p.val;
if (!onlyIfAbsent) // 判断
// 赋值结点value值
p.val = value;
}
}
}
}
if (binCount != 0) { // binCount不为0
if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大于等于转化为红黑树的阈值
// 进行转化
treeifyBin(tab, i);
if (oldVal != null) // 旧值不为空
// 返回旧值
return oldVal;
break;
}
}
}
// 增加binCount的数量
addCount(1L, binCount);
return null;
}

1.8的put中涉及table的初始化,树的转化,扩容等方法 initTable、tabAt、casTabAt、helpTransfer、putTreeVal、treeifyBin、addCount函数。后面会一一介绍

1.8putVal方法流程总结

1.当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组

2.通过计算hash值来确定放在数组的哪个位置

3.如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来

4.如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制

5.最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作

6.然后判断当前取出的节点位置存放的是链表还是树

7.如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,

则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾

8.如果是树的话,则调用putTreeVal方法把这个元素添加到树中去

最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,

9.则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组

JDK1.8initTable()

在使用的时候才会初始化数组

并发问题是通过CAS操控sizeCtl变量实现的。

initTable源码

private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { //第一次put的时候,table还没被初始化,进入while
if ((sc = sizeCtl) < 0) //sizeCtl初始值为0,当小于0的时候表示在别的线程在初始化表或扩展表
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //SIZECTL:表示当前对象的内存偏移量,sc表示期望值,-1表示要替换的值,设定为-1表示要初始化表了
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //指定了大小的时候就创建指定大小的Node数组,否则创建指定大小(16)的Node数组
@SuppressWarnings(“unchecked”)
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc; //初始化后,sizeCtl长度为数组长度的3/4
}
break;
}
}
return tab;
}

initTable流程

1.如果sizeCtl小于0,说明别的数组正在进行初始化,则让出执行权

2.如果sizeCtl大于0的话,则初始化一个大小为sizeCtl的数组

3.否则的话初始化一个默认大小(16)的数组

4.然后设置sizeCtl的值为数组长度的3/4

tabAt函数

@SuppressWarnings(“unchecked”)
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
//强制去读取主存中的数据而不是线程自己的本地工作内存,保证数据是最新的
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);

}

此函数返回table数组中下标为i的结点,可以看到是通过Unsafe对象通过反射获取的,getObjectVolatile的第二项参数为下标为i的偏移地址。

casTabAt函数

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
//使用cas的比较交换
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

此函数用于比较table数组下标为i的结点是否为c,若为c,则用v交换操作。否则,不进行交换操作。

helpTransfer函数

/**

  • Helps transfer if a resize is in progress.
  • 如果正在调整大小则帮忙转移
    */
    //将旧的数组中的元素复制到新的数组中
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    //旧数组不为空且nextTable也不空的情况下才能复制
    if (tab != null && (f instanceof ForwardingNode) &&
    (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
    int rs = resizeStamp(tab.length);
    while (nextTab == nextTable && table == tab &&
    (sc = sizeCtl) < 0) {
    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
    sc == rs + MAX_RESIZERS || transferIndex <= 0)
    break;
    //cas操作保证线程安全
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab);//调用扩容方法
    break;
    }
    }
    return nextTab;
    }
    return table;
    }

用于在扩容时将table表中的结点转移到nextTable中。

putTreeVal函数

final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;// 定义k的Class对象
// 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点
boolean searched = false;
// 从根节点开始遍历,没有终止条件,只能从内部退出
for (TreeNode<K,V> p = root;😉 {
int dir, ph; K pk;
//如果根节点为空,那么赋值当前节点就是根节点,结束循环
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
// 如果当前节点hash 大于 指定key的hash值
else if ((ph = p.hash) > h)
dir = -1;//要添加的元素应该放置在当前节点的左侧
else if (ph < h)//小于
dir = 1;//在右侧
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))// 如果当前节点的键对象 和 指定key对象相同
return p; // 那么就返回当前节点对象,在外层方法会对v进行写入

// 走到这一步说明 当前节点的hash值 和 指定key的hash值 是相等的,但是equals不等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {

// 走到这里说明:指定key没有实现comparable接口 或者 实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)

if (!searched) {// 如果还没有比对过当前节点的所有子节点
TreeNode<K,V> q, ch;// 定义要返回的节点、和子节点
searched = true;//标识为已经遍历过
/*

  • 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
  • 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
  • find 方法内部还会有递归调用
    */
    if (((ch = p.left) != null &&
    (q = ch.findTreeNode(h, k, kc)) != null) ||
    ((ch = p.right) != null &&
    (q = ch.findTreeNode(h, k, kc)) != null))
    return q;
    }
    // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
    dir = tieBreakOrder(k, pk);// 再比较一下当前节点键和指定key键的大小
    }

TreeNode<K,V> xp = p; // 定义xp指向当前节点
/*

  • 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
  • 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
  • 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
    */
    if ((p = (dir <= 0) ? p.left : p.right) == null) {
    // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
    TreeNode<K,V> x, f = first; // 获取当前节点的next节点
    first = x = new TreeNode<K,V>(h, k, v, f, xp);// 创建一个新的树节点
    if (f != null)
    f.prev = x;
    if (dir <= 0)
    xp.left = x; // 左孩子指向到这个新的树节点
    else
    xp.right = x;// 右孩子指向到这个新的树节点
    if (!xp.red)
    x.red = true;
    else {
    lockRoot();//加锁
    try {
    root = balanceInsertion(root, x);// 重新平衡,以及新的根节点置顶
    } finally {
    unlockRoot();//解锁
    }
    }
    break;
    }
    }
    assert checkInvariants(root);
    return null;
    }

此函数用于将指定的hash、key、value值添加到红黑树中,若已经添加了,则返回null,否则返回该结点。

treeifyBin函数

当数组长度小于64的时候,扩张数组长度一倍,否则的话把链表转为树

private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
System.out.println(“treeifyBin方\t==>数组长:”+tab.length);
if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY 64
tryPresize(n << 1); // 数组扩容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) { //使用synchronized同步器,将该节点出的链表转为树
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null; //hd:树的头(head)
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) //把Node组成的链表,转化为TreeNode的链表,头结点任然放在相同的位置
hd = p; //设置head
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));//把TreeNode的链表放入容器TreeBin中
}
}
}
}
}

此函数用于将桶中的数据结构转化为红黑树,其中,值得注意的是,当table的长度未达到阈值时,会进行一次扩容操作,该操作会使得触发treeifyBin操作的某个桶中的所有元素进行一

次重新分配,这样可以避免某个桶中的结点数量太大。

tryPresize方法

private final void tryPresize(int size) {
/*

  • MAXIMUM_CAPACITY = 1 << 30
  • 如果给定的大小大于等于数组容量的一半,则直接使用最大容量,
  • 否则使用tableSizeFor算出来
  • 后面table一直要扩容到这个值小于等于sizeCtrl(数组长度的3/4)才退出扩容
    /
    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;
    /
  • 如果数组table还没有被初始化,则初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小的数组
  • 初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4
  • 为什么要在扩张的地方来初始化数组呢?
  • 这是因为如果第一次put的时候不是put单个元素,
  • 而是调用putAll方法直接put一个map的话,在putALl方法中没有调用initTable方法去初始化table,
  • 而是直接调用了tryPresize方法,所以这里需要做一个是不是需要初始化table的判断
    /
    if (tab == null || (n = tab.length) == 0) {
    n = (sc > c) ? sc : c;
    if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //初始化tab的时候,把sizeCtl设为-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;
    }
    }
    }
    /
  • 一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出
  • 所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍
    /
    else if (c <= sc || n >= MAXIMUM_CAPACITY) {
    break; //退出扩张
    }
    else if (tab == table) {
    int rs = resizeStamp(n);
    /
  • 如果正在扩容Table的话,则帮助扩容
  • 否则的话,开始新的扩容
  • 在transfer操作,将第一个参数的table中的元素,移动到第二个元素的table中去,
  • 虽然此时第二个参数设置的是null,但是,在transfer方法中,当第二个参数为null的时候,
  • 会创建一个两倍大小的table
    /
    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;
    /
  • transfer的线程数加一,该线程将进行transfer的帮忙
  • 在transfer的时候,sc表示在transfer工作的线程数
    /
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
    transfer(tab, nt);
    }
    /
  • 没有在初始化或扩容,则开始扩容
    */
    else if (U.compareAndSwapInt(this, SIZECTL, sc,
    (rs << RESIZE_STAMP_SHIFT) + 2)) {
    transfer(tab, null);
    }
    }
    }
    }

在tryPresize方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容。

addCount方法

rivate final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { // counterCells不为空或者比较交换失败
CounterCell a; long v; int m;
// 无竞争标识
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // cas
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//是否需要进行扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}

此函数主要完成binCount的值加1的操作。

扩容方法

jdk1.8transfer方法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //MIN_TRANSFER_STRIDE 用来控制不要占用太多CPU
stride = MIN_TRANSFER_STRIDE; // subdivide range //MIN_TRANSFER_STRIDE=16
/*

  • 如果复制的目标nextTab为null的话,则初始化一个table两倍长的nextTab
  • 此时nextTable被设置值了(在初始情况下是为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 = nextTab;
    transferIndex = n;
    }
    int nextn = nextTab.length;
    /
  • 创建一个fwd节点,这个是用来控制并发的,当一个节点为空或已经被转移之后,就设置为fwd节点
  • 这是一个空的标志节点
    /
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    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); //设置sizeCtl为扩容后的0.75
    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) //数组中把null的元素设置为ForwardingNode节点(hash值为MOVED[-1])
    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) { //该节点的hash值大于等于0,说明是一个Node节点
    /
  • 因为n的值为数组的长度,且是power(2,x)的,所以,在&操作的结果只可能是0或者n
  • 根据这个规则
  •     0-->  放在新表的相同位置
    
  •     n-->  放在新表的(n+原来位置)
    

/
int runBit = fh & n;
Node<K,V> lastRun = f;
/

  • lastRun 表示的是需要复制的最后一个节点
  • 每当新节点的hash&n -> b 发生变化的时候,就把runBit设置为这个结果b
  • 这样for循环之后,runBit的值就是最后不变的hash&n的值
  • 而lastRun的值就是最后一次导致hash&n 发生变化的节点(假设为p节点)
  • 为什么要这么做呢?因为p节点后面的节点的hash&n 值跟p节点是一样的,
  • 所以在复制到新的table的时候,它肯定还是跟p节点在同一个位置
  • 在复制完p节点之后,p节点的next节点还是指向它原来的节点,就不需要进行复制了,自己就被带过去了
  • 这也就导致了一个问题就是复制后的链表的顺序并不一定是原来的倒序
    /
    for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n; //n的值为扩张前的数组的长度
    if (b != runBit) {
    runBit = b;
    lastRun = p;
    }
    }
    if (runBit == 0) {
    ln = lastRun;
    hn = null;
    }
    else {
    hn = lastRun;
    ln = null;
    }
    /
  • 构造两个链表,顺序大部分和原来是反的
  • 分别放到原来的位置和新增加的长度的相同位置(i/n+i)
    /
    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)
    /
  • 假设runBit的值为0,
  • 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同为0的节点)设置到旧的table的第一个hash计算后为0的节点下一个节点
  • 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点
    */
    ln = new Node<K,V>(ph, pk, pv, ln);
    else

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

😝朋友们如果有需要的话,可以联系领取~

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

2️⃣视频配套工具&国内外网安书籍、文档
① 工具

② 视频

image1

③ 书籍

image2

资源较为敏感,未展示全面,需要的最下面获取

在这里插入图片描述在这里插入图片描述

② 简历模板

在这里插入图片描述

因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值