ConcurrentHashMap 扩容机制(源码解析)

目录

1. 扩容触发的条件

1.1. addCount() -- 累加计数器和检查扩容

1.2. treeifyBin() -- 数组小于 64,先扩容

1.3. tryPresize() -- 预调整哈希表容量

2. 开始扩容

2.1. transfer() -- 计算每个线程迁移的长度

2.2. transfer() -- 构建新数组

2.3. transfer() -- 线程领取迁移任务

2.4. transfer() -- 判断扩容是否已经结束

2.5. transfer() -- 迁移数据(链表)

2.6. helpTransfer() -- 协助扩容

3. 细节讲解

3.1. 扩容戳

3.2. 首次扩容为什么计数是 +2 而不是 +1

4. 流程图


1. 扩容触发的条件

  1. 插入导致容量超过阈值:当执行插入操作时,如果插入新元素后,表中的元素数量超过了当前容量与负载因子乘积所确定的阈值,就会触发扩容。
  2. treeifyBin() 方法中的扩容:当某个桶中的链表长度超过阈值(默认为 8)时,会尝试将链表转换为红黑树。然而,如果当前数组的容量小于 64,treeifyBin() 方法会选择进行扩容而不是树化,这会触发扩容。
  3. putAll() 方法中的扩容:当需要在短时间内插入大量元素时,tryPresize 方法可能会被调用以提前调整容量。此方法会计算所需的新容量,并通过调用 transfer() 方法来进行扩容。

1.1. addCount() -- 累加计数器和检查扩容

在存储完元素后,会更新计数器以及检查扩容

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 存储操作...
    
    addCount(1L, binCount);
    return null;
}

  • 更新计数:
    • 首先尝试通过 CAS 操作直接更新 baseCount。如果 counterCells 数组不为空(说明已经有竞争)或 CAS 操作失败,则进入更复杂的路径来更新计数
  • 处理竞争:
    • 如果 counterCells 存在且当前线程的计数单元(CounterCell)可以更新,则直接更新。
    • 如果更新失败(可能由于竞争),则调用 fullAddCount() 方法。这是一个更复杂的方法,用于处理高竞争情况下的计数更新。
  • 检查并触发扩容:
    • 如果 check 大于或等于 0,则可能需要检查是否需要扩容。
    • 如果当前元素数量 s 超过了 sizeCtl,并且表的容量小于最大容量,则可能需要进行扩容。
    • 扩容通过 transfer 方法完成。在扩容过程中,多个线程可以协作完成数据的迁移。
    • 使用 CAS 操作来控制并发扩容过程,确保线程安全

int check

  • 当 check 大于等于 0 时,表示需要在更新计数后进行检查,以确定是否需要扩容。
  • 当 check 小于 0 时,表示不需要进行扩容检查。这种情况下,通常只更新计数,而不考虑是否需要扩容。

在putVal()方法中,check 是传入 binCount(某个桶的操作次数)

CounterCell[] as

作用:

  • ConcurrentHashMap 通过维护一个 CounterCell 数组,使每个线程可以在不同的 CounterCell 上进行计数更新,从而减少竞争。
  • 当多个线程同时更新计数时,ConcurrentHashMap 会尝试让每个线程更新不同的 CounterCell。最终,所有 CounterCell 的值会被汇总以得到总的计数值。

工作机制:

  • 初始状态:在竞争不严重的情况下,ConcurrentHashMap 会直接更新一个共享的计数器 baseCount。
  • 竞争加剧:如果检测到竞争(例如,更新 baseCount 的 CAS 操作失败),则会初始化 counterCells 数组,并将计数分散到不同的 CounterCell 中。
  • 线程映射:每个线程通过哈希计算找到对应的 CounterCell。如果哈希冲突导致多个线程访问同一个 CounterCell,ConcurrentHashMap 会尝试重新分配或扩展 counterCells 数组。
  • 计数汇总:当需要获取总的元素数量时,会将 baseCount 和所有 CounterCell 的值相加。
private final void addCount(long x, int check) {
    CounterCell[] as; 
    long b, s;

    // 无竞争状态下counterCells == null 且 CAS baseCount(累加1)成功,不会进入下面代码块
    // //有竞争进入则进入以下代码块
    if ((as = counterCells) != null 
        || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        
        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))) {

            // 高竞争环境下更新计数器
            fullAddCount(x, uncontended);
            return;
        }

        // 如果不需要检查(check <= 1),直接返回
        if (check <= 1){
            return;
        }

        // 重新计算总数
        s = sumCount();
    }

    // 如果需要检查并可能触发扩容
    if (check >= 0) {
        Node<K,V>[] tab, nt; 
        int n, sc;

        // 如果元素个数大于等于sizeCtl,需要扩容
        while (s >= (long)(sc = sizeCtl) 
               && (tab = table) != null 
               && (n = tab.length) < MAXIMUM_CAPACITY) {

            // 计算扩容标记
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;

            // 第一次尝试扩容是不会进入下面这个if的
            if (sc < 0) {
                // 如果 sc == 扩容戳 + 最大扩容线程,表示扩容的线程数已经到最大值了
                // sc == rs + 1,表示扩容已经结束了,
                // nextTable 表示扩容用的新table,如果它为空表示没有在进行扩容或者已经扩容结束
                // transferIndex 表示扩容索引,如果小于等于0表示没有在进行扩容操作或扩容结束
                if (sc == rs + MAX_RESIZERS 
                    || sc == rs + 1 
                    ||(nt = nextTable) == null 
                    || transferIndex <= 0){
                    break;
                }

                // 增加扩容线程的数量并执行 transfer
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)){
                    transfer(tab, nt);
                }
            }else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2)){
                // 初始化扩容
                transfer(tab, null);
            }

            // 重新计算总数
            s = sumCount();
        }
    }
}

1.2. treeifyBin() -- 数组小于 64,先扩容

在链表长度大于等于 8 时,尝试将链表转为红黑树

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; 
    int n, sc;
    
    // 数组不能为空
    if (tab != null) {
        // 数组的长度n,是否小于64
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY){
            // 如果数组长度小于64,不能将链表转为红黑树,先尝试扩容操作
            // 这里的扩容传入的数据是当前数据的2倍
            tryPresize(n << 1);
        }
        
        // ...
    }
}

1.3. tryPresize() -- 预调整哈希表容量

目的是在预计将要插入大量元素时,提前调整 ConcurrentHashMap 的容量,以减少后续扩容的次数和开销

private final void tryPresize(int size) {
    // 计算预期的容量 c	通常是 size 的 1.5 倍加 1
    // 如果达到最大容量的一半,则直接设置为最大容量
    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;
        
        // 如果当前哈希表未初始化或长度为0,则进行初始化
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;

            // 通过 CAS 设置 sizeCtl 为 -1,表示正在初始化
            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;

                        // 设置 sizeCtl 为新容量的 0.75
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        // 如果目标容量 c 小于等于当前 sizeCtl 或者当前容量达到最大值,则无需扩容
        }else if (c <= sc || n >= MAXIMUM_CAPACITY){
            break;
        }else if (tab == table) {
            // 扩容戳,生成与当前数组长度相关的标记
            int rs = resizeStamp(n);

            // 如果 sizeCtl 是负数,说明有线程正在进行扩容,当前线程可以协助扩容
            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 增加 sizeCtl,表示增加一个线程来协助扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)){
                    transfer(tab, nt);
                }
            }else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)){
                // 调用transfer方法,并且将第二个参数设置为null,就代表是第一次来扩容
                transfer(tab, null);
            }
        }
    }
}

2. 开始扩容

代码是连续的,为了更好理解,根据每段代码的作用分段解释

这里假设 NCPU = 16,旧数组长度为 32

下文的变量都是处理完的数值

2.1. transfer() -- 计算每个线程迁移的长度

NCPU = 16:系统当前可用的处理器数量

n = 32:旧数组长度

stride = 16:每次迁移的范围

// 表示系统当前可用的处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();

// nextTab 表示扩容用的新 table,第一次扩容 nextTab 是 null
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length;
    // 每个线程一次性迁移多少数据到新数组
    int stride;

    // 这里的步长可以理解为每次迁移的范围
    // 最小步长 MIN_TRANSFER_STRIDE = 16
    // 如果线程数只有 1 的话,直接就是原数组长度
    // 如果算出来每个线程的长度小于 16 的话,直接使用最小步长 16
    // 大于 16 则使用 (n >>> 3) / NCPU
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE){
        stride = MIN_TRANSFER_STRIDE;
    }

    // ...

2.2. transfer() -- 构建新数组

transferIndex = 32:初始值设置为旧表的长度 n(32),表示整个表尚未被处理

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

    // 第一个进来扩容的线程需要把新数组构建出来
    if (nextTab == null) {
        try {
            // 新 table 的长度是原 table 长度 * 2
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        
        // 这两个成员变量都是被volatile修饰的,保证不同线程操作扩容时的可见性
        nextTable = nextTab;
        transferIndex = n;
    }

    // ...
}

2.3. transfer() -- 线程领取迁移任务

nextn = 64:新数组的长度

transferIndex = 16:尚未被处理的旧表索引的最高位置

advance = false:表示当前线程是否需要继续处理任务

finishing = false:用于标识迁移是否结束,在迁移完成后设置为 true,以停止扩容过程

i = 31:表示当前处理桶的位置

bound = 16:用于限制当前线程可以处理的桶的范围,在任务分配时,bound 被设置为当前线程可以处理的最高桶索引

// 新数组长度
int nextn = nextTab.length;

//当扩容过程中遇到需要移动的节点时,可以将这些节点替换为 ForwardingNode,从而标识该位置正在进行扩容操作
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

// advance:true,代表当前线程需要接收任务,然后再执行迁移
// 如果为false,代表已经接收完任务
boolean advance = true;

// 标识迁移是否结束
boolean finishing = false;

for (int i = 0, bound = 0;;) {
    Node<K,V> f; 
    int fh;
    while (advance) {
        int nextIndex, nextBound;
        // 第一次不会进入这个if块
        if (--i >= bound || finishing){
            advance = false;
        }
        
        // 第一次也不会进入这里
        // 判断 transferIndex 是否小于等于0,代表没有任务可领取,结束了
        // 在线程领取任务会,会对 transferIndex 进行修改,修改为 transferIndex - stride
        // 在任务都领取完之后,transferIndex 肯定是小于等于 0 的,代表没有迁移数据的任务可以领取
        else if ((nextIndex = transferIndex) <= 0) {
            // 扩容结束
            i = -1;
            advance = false;
        }
        
        // nextIndex = 32, stride = 16
        // nextIndex > stride ? nextIndex - stride : 0 当前的nextIndex是否大于每个线程切割的
        // 将 TRANSFERINDEX 从 nextIndex 变成16
        else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, 
                                     nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
            // 对 bound 赋值(16)
            bound = nextBound;
            
            // 对 i 赋值(31)
            i = nextIndex - 1;
            
            // 赋值,代表当前领取任务结束
            // 该线程当前领取的任务是(16~31)
            advance = false;
        }
    }
}

2.4. transfer() -- 判断扩容是否已经结束

i = 31:当前处理桶的索引位置

n = 32:旧数组长度

// 情况1:i < 0,分为以下两种情况:
//  1. 迁移已经结束:初始时i = 0,在第一个if块中自减为-1,但未进入最后一个if块分配迁移区间,此时i仍为-1
//  2. 当前线程没有获得任务
// 情况2:i >= n:表示迁移的索引位置不可能大于或等于数组长度,因此不会成立
// 情况3:i + n >= nextn:由于i的最大值是数组索引的最大值,因此此条件也不会成立
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    
    // 第一次到这里:必定是 false
    // 如果再次到这里,并且最后一个扩容的线程也完成了扫描
    if (finishing) {
        nextTable = null;
        table = nextTab;
        
        // 更新 sizeCtl 为新数组长度的0.75倍(扩容阈值)
        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重新变成原数组的长度
        // 重新进行一轮校验
        i = n; 
    }
}

2.5. transfer() -- 迁移数据(链表)

Node<K,V> f:当前索引位置 i 处的节点,用于检查和处理当前桶的内容

ForwardingNode<K,V> fwd:标记当前桶位置已经被处理,ForwardingNode 的哈希值为 MOVED,用于标识该位置正在进行或已完成迁移

fh:当前节点的哈希值

ln:低位链表的头节点,存储哈希值与 n 的按位与结果为 0 的节点

hn:高位链表的头节点,存储哈希值与 n 的按位与结果为 n 的节点

runBit:用于确定节点在新数组中的位置,runBit 计算方式为fh & n,结果为 0 或 n

lastRun:用于追踪最后一个相同 runBit 的节点,节点 lastRun 及其后续节点将直接迁移到新数组中

这里迁移链表的操作和 HashMap 差不多,都是将节点通过哈希值分为两条链表,不过这里利用了 LastRun 机制,通过记录最后一个 runBit 相同的节点,减少了链表节点的冗余计算和复制

// 如果当前索引 i 处没有数据,则不需要迁移
else if ((f = tabAt(tab, i)) == null){
    // 使用 CAS 操作将 i 位置标记为fwd,fwd 的哈希值为 MOVED,表示该位置已处理
    advance = casTabAt(tab, i, null, fwd);
}
// 如果当前位置的哈希值为 MOVED,说明数据已经迁移完成
else if ((fh = f.hash) == MOVED){
    // 直接标记为已处理,主要用于最后一个扩容线程的检查
    advance = true;
}else {
    // 锁定当前节点以确保线程安全
    synchronized (f) {
        // 再次检查当前节点是否未改变
        if (tabAt(tab, i) == f) {
            // 低位链表节点
            Node<K,V> ln = null;
            // 高位链表节点
            Node<K,V> hn = null;
            
            // 如果当前节点的哈希值为正常值
            if (fh >= 0) {
                // 计算当前节点的 runBit(0或n)
                int runBit = fh & n; 
                // 用于追踪最后一个相同 runBit 的节点
                Node<K,V> lastRun = f;
                
                // 遍历链表以确定最后一个 runBit 相同的节点
                for (Node<K,V> p = f.next; p != null; p = p.next) {

                    int b = p.hash & n; 
                    // 如果 b 值不同于 runBit,更新 runBit 和 lastRun
                    if (b != runBit) {
                        runBit = b;
                        lastRun = p;
                    }
                }
                
                // 根据最后的runBit决定ln和hn的指向
                if (runBit == 0) {
                    ln = lastRun;
                } else {
                    hn = lastRun;
                }
                
                // 从链表头遍历到lastRun,将节点分配到ln或hn
                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);
                    }
                }
                
                // 将新数组的i位置设置为ln链表
                setTabAt(nextTab, i, ln);
                
                // 将新数组的i+n位置设置为hn链表
                setTabAt(nextTab, i + n, hn);
                
                // 将原数组的i位置标记为fwd,表示迁移完成
                setTabAt(tab, i, fwd);
                
                // 设置advance为true,以便继续处理下一个节点
                advance = true;
            }
        }
    }
}

2.6. helpTransfer() -- 协助扩容

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; 
    int sc;

    // 1. 老数组不为 null
    // 2. f 是 ForwardingNode 类型,用于指示正在进行的扩容。
    // 3. 新数组不为 null(获取新数组并赋给 nextTab)。
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 获取扩容标识戳
        int rs = resizeStamp(tab.length);
        
        // 进入循环以协助扩容
        // 1. nextTab仍是当前正在使用的新数组。
        // 2. table仍是当前的老数组。
        // 3. sizeCtl为负数,表示正在扩容。
        while (nextTab == nextTable 
               && table == tab 
               && (sc = sizeCtl) < 0) {
            
            // 检查扩容状态:
            // 1. 扩容标识戳是否一致。
            // 2. transferIndex是否小于0,表示所有扩容任务已被领取。
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs 
                || sc == rs + 1 
                || sc == rs + MAX_RESIZERS 
                || transferIndex <= 0){
                
                break;
            }
            
            // 如果还有未领取的任务,尝试通过CAS操作增加sizeCtl,表示协助扩容的线程数加一。
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 调用transfer方法实际执行扩容任务
                transfer(tab, nextTab);
                break;
            }
        }
        // 返回新数组
        return nextTab;
    }
    // 如果条件不满足,返回当前表
    return table;
}

3. 细节讲解

在前面部分其实就是对源码进行解读,但阅读过程中也有一些问题需要深入研究的

3.1. 扩容戳

扩容戳的作用:

  • 唯一标识:扩容戳为每次扩容操作生成一个唯一的标识符。它确保当前的扩容操作与其他可能的扩容操作区分开来
  • 协助判断:在多线程环境下,当一个线程发现扩容正在进行时,它可以通过扩容戳判断是否需要协助当前的扩容操作
  • 标识扩容状态:sizeCtl 的高位部分包含扩容戳,而低位部分用于表示当前参与扩容的线程数量

在实际使用中,扩容戳会左移 16 位,然后赋值给 sizeCtl。这样,计算出的扩容戳实际上位于 sizeCtl 的高 16 位。高 16 位用于标识当前扩容操作的唯一性,而低 16 位用于记录参与扩容的线程数。

确保第 16 位为 1 的目的是为了保证 sizeCtl 变量为负数。由于扩容戳在使用时会左移 16 位,这确保了最高位始终为 1,从而使 sizeCtl 保持负数状态。

生成扩容戳代码:

// 扩容戳的计算通常是通过 resizeStamp 方法,该方法根据旧数组的长度计算一个标识符
// 这确保了在不同数组长度下,扩容戳是不同的
int rs = resizeStamp(n);

private static int RESIZE_STAMP_BITS = 16;
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

// 获取当前数据转成二进制后的最高位的 1 前的 0 的个数
public static int numberOfLeadingZeros(int i) {
    if (i == 0)
        return 32;
    int n = 1;
    if (i >>> 16 == 0) { n += 16; i <<= 16; }
    if (i >>> 24 == 0) { n +=  8; i <<=  8; }
    if (i >>> 28 == 0) { n +=  4; i <<=  4; }
    if (i >>> 30 == 0) { n +=  2; i <<=  2; }
    n -= i >>> 31;
    return n;
}

例子

假如 n = 32

Integer.numberOfLeadingZeros(n)

  • n = 32 => 10 0000
  • 最高位的 "1" 位位于 第 6 位
  • 因为 int 是32 位,前面有 26 个 "0"
  • return 26

1 << (RESIZE_STAMP_BITS - 1)

  • RESIZE_STAMP_BITS = 16
  • 1 << (16 - 1) 即 1 << 15,结果是二进制的 1000 0000 0000 0000(即 32768)

位或运算 |

  • 将 26(即二进制的 0000 0000 0001 1010)与 32768(即二进制的 1000 0000 0000 0000)进行位或操作
  • 结果是 1000 0000 0001 1010,即十进制的 32794

3.2. 首次扩容为什么计数是 +2 而不是 +1

else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2)){
    // 初始化扩容
    transfer(tab, null);
}

前面已经说过扩容戳的作用

  • 高 16 位用于标识当前扩容操作。
  • 低 16 位用于记录参与扩容的线程数。

了解这两个条件后,更容易理解扩容戳的处理过程。扩容戳最终会被赋值给 sizeCtl。在 sizeCtl 中,负数表示正在进行扩容。通过将扩容戳左移 16 位,确保最高位为 1,此时低 16 位全部为 0。

低 16 位需要记录扩容线程数,因此通常需要加 1。不过,这里加的是 2,这是因为 sizeCtl 中的 -1 已经被使用,用于标识当前有线程准备进行扩容。如果仅加 1,可能会与标志位发生冲突。为了避免这种冲突,初始化记录扩容线程数时,需要加 2

4. 流程图

内容概要:本文档详细介绍了在三台CentOS 7服务器(IP地址分别为192.168.0.157、192.168.0.158和192.168.0.159)上安装和配置Hadoop、Flink及其他大数据组件(如Hive、MySQL、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala)的具体步骤。首先,文档说明了环境准备,包括配置主机名映射、SSH免密登录、JDK安装等。接着,详细描述了Hadoop集群的安装配置,包括SSH免密登录、JDK配置、Hadoop环境变量设置、HDFS和YARN配置文件修改、集群启动与测试。随后,依次介绍了MySQL、Hive、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala和Flink的安装配置过程,包括解压、环境变量配置、配置文件修改、服务启动等关键步骤。最后,文档提供了每个组件的基本测试方法,确保安装成功。 适合人群:具备一定Linux基础和大数据组件基础知识的运维人员、大数据开发工程师以及系统管理员。 使用场景及目标:①为大数据平台建提供详细的安装指南,确保各组件能够顺利安装和配置;②帮助技术人员快速掌握Hadoop、Flink等大数据组件的安装与配置,提升工作效率;③适用于企业级大数据平台的建与维护,确保集群稳定运行。 其他说明:本文档不仅提供了详细的安装步骤,还涵盖了常见的配置项解释和故障排查建议。建议读者在安装过程中仔细阅读每一步骤,并根据实际情况调整配置参数。此外,文档中的命令和配置文件路径均为示例,实际操作时需根据具体环境进行适当修改。
在无线通信领域,天线阵列设计对于信号传播方向和覆盖范围的优化至关重要。本题要求设计一个广播电台的天线布局,形成特定的水平面波瓣图,即在东北方向实现最大辐射强度,在正东到正北的90°范围内辐射衰减最小且无零点;而在其余270°范围内允许出现零点,且正西和西南方向必须为零。为此,设计了一个由4个铅垂铁塔组成的阵列,各铁塔上的电流幅度相等,相位关系可自由调整,几何布置和间距不受限制。设计过程如下: 第一步:构建初级波瓣图 选取南北方向上的两个点源,间距为0.2λ(λ为电磁波波长),形成一个端射阵。通过调整相位差,使正南方向的辐射为零,计算得到初始相位差δ=252°。为了满足西南方向零辐射的要求,整体相位再偏移45°,得到初级波瓣图的表达式为E1=cos(36°cos(φ+45°)+126°)。 第二步:构建次级波瓣图 再选取一个点源位于正北方向,另一个点源位于西南方向,间距为0.4λ。调整相位差使西南方向的辐射为零,计算得到相位差δ=280°。同样整体偏移45°,得到次级波瓣图的表达式为E2=cos(72°cos(φ+45°)+140°)。 最终组合: 将初级波瓣图E1和次级波瓣图E2相乘,得到总阵的波瓣图E=E1×E2=cos(36°cos(φ+45°)+126°)×cos(72°cos(φ+45°)+140°)。通过编程实现计算并绘制波瓣图,可以看到三个阶段的波瓣图分别对应初级波瓣、次级波瓣和总波瓣,最终得到满足广播电台需求的总波瓣图。实验代码使用MATLAB编写,利用polar函数在极坐标下绘制波瓣图,并通过subplot分块显示不同阶段的波瓣图。这种设计方法体现了天线阵列设计的基本原理,即通过调整天线间的相对位置和相位关系,控制电磁波的辐射方向和强度,以满足特定的覆盖需求。这种设计在雷达、卫星通信和移动通信基站等无线通信系统中得到了广泛应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值