有十亿个数据如何取到最小的十个

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了有十亿个数据如何取到最小的十个相关的知识,希望对你有一定的参考价值。

这是典型的大数据分布式计算的案例,十亿量级的数据还是很大的,放在一个机器上处理排序会很慢,但是,如果才用分布式计算,并设计一套算法,三个臭皮匠顶个诸葛亮,做算的速度可以提升很多,每个机器算一部分数据,将结果汇总到一台机器再进行计算,前者是map,后者是reduce,这个框架就是Hadoop,现在比较流行的大数据框架,可以了解了解 参考技术A 建立十个数从小到大的数组,首先都赋值第一个数。

然后第二个数开始与十个之最大的比较,小于就插入,废弃最大的一个。

ConcurrentHashMap中有十个提升性能的细节,你都知道吗?

相关阅读:一个90后员工猝死的全过程

一些题外话

如何在高并发下提高系统吞吐是所有后端开发者追求的目标,Java并发的开创者Doug Lea在Java 7 ConcurrentHashMap的设计中给出了一些参考答案,本文详细的总结了ConcurrentHashMap源码中影响并发性能的十个细节,有常见的自旋锁,CAS的使用,也有延迟写内存,volatile语义退化等不常见的技巧,希望对大家的开发设计有所帮助。

由于ConcurrentHashMap的内容比较多,而且Java 7Java 8两个版本的实现相差比较大,如果采用我们上篇中对比的那种行文思路,在有限的篇幅中难免会遗漏一些细节,因此我决定采用两篇文章去详细阐述两个版本中ConcurrentHashMap技术细节,不过为了帮助读者体系化的理解,三篇文章(包含HashMap的那一篇)整体文章的结构将保持一致。

把书读薄

《阿里巴巴Java开发手册》的作者孤尽对ConcurrentHashMap的设计十分推崇,他说:“ConcurrentHashMap源码是学习Java代码开发规范的一个非常好的学习材料,我建议同学们可以时常去看一看,总会有新的收货的”,相信大家平常也能听到很多对于ConcurrentHashMap设计的溢美之词,在展开隐藏在ConcurrentHashMap所有小秘密之前,大家在大脑中首先要有这样的一幅图:

img

对于Java 7来说,这张图已经能完全把ConcurrentHashMap的架构说清楚了:

  1. ConcurrentHashMap是一个线程安全的Map实现,其读取不需要加锁,通过引入Segment,可以做到写入的时候加锁力度足够小

  2. 由于引入了SegmentConcurrentHashMap在读取和写入的时候需要需要做两次哈希,但这两次哈希换来的是更细力粒度的锁,也就意味着可以支持更高的并发

  3. 每个桶数组中的key-value对仍然以链表的形式存放在桶中,这一点和HashMap是一致的。

把书读厚

关于Java 7ConcurrentHashMap的整体架构,用上面三两句话就可以概括,这张图应该很快就可以在大家的大脑中留下印象,接下来我们通过几个问题来尝试吸引大家继续看下去,把书读厚:

  1. ConcurrentHashMap的哪些操作需要加锁?

  2. ConcurrentHashMap的无锁读是如何实现的?

  3. 在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?

  4. 在有Segment存在的前提下,应该如何扩容的?

在上一篇文章中我们总结了HashMap中最重要的点有四个:初始化数据寻址-hash方法数据存储-put方法,扩容-resize方法,对于ConcurrentHashMap来说,这四个操作依然是最重要的,但由于其引入了更复杂的数据结构,因此在调用size()查看整个ConcurrentHashMap的数量大小的时候也有不小的挑战,我们也会重点看下Doug Lea在size()方法中的设计

初始化

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    // 保证ssize是大于concurrencyLevel的最小的2的整数次幂
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 寻址需要两次哈希,哈希的高位用于确定segment,低位用户确定桶数组中的元素
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

初始化方法中做了三件重要的事:

  1. 确定了segments的数组的大小ssizessize根据入参concurrencyLevel确定,取大于concurrencyLevel的最小的2的整数次幂

  2. 确定哈希寻址时的偏移量,这个偏移量在确定元素在segment数组中的位置时会用到

  3. 初始化segment数组中的第一个元素,元素类型为HashEntry的数组,这个数组的长度为initialCapacity / ssize,即初始化大小除以segment数组的大小,segment数组中的其他元素在后续put操作时参考第一个已初始化的实例初始化

static final class HashEntry<K,V> {
    final int hash; 
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next; 
 
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    final void setNext(HashEntry<K,V> n) {
        UNSAFE.putOrderedObject(this, nextOffset, n);
    }
}

这里的HashEntryHashMap中的HashEntry作用是一样的,它是ConcurrentHashMap的数据项,这里要注意两个细节:

细节一:

HashEntry的成员变量valuenext是被关键字volatile修饰的,也就是说所有线程都可以及时检查到其他线程对这两个变量的改变,因而可以在不加锁的情况下读取到这两个引用的最新值

细节二:

HashEntrysetNext方法中调用了UNSAFE.putOrderedObject,这个接口是属于sun安全库中的api,并不是J2SE的一部分,它的作用和volatile恰恰相反,调用这个api设值是使得volatile修饰的变量延迟写入主存,那到底是什么时候写入主存呢?

JMM中有一条规定:

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

后文在讲put方法的时候我们再详细看setNext的用法

哈希

由于引入了segment,因此不管是调用get方法读还是调用put方法写,都需要做两次哈希,还记得在上文我们讲初始化的时候系统做了一件重要的事:

  • 确定哈希寻址时的偏移量,这个偏移量在确定元素在segment数组中的位置时会用到

没错就是这段代码:

this.segmentShift = 32 - sshift;

这里用32去减是因为int型的长度是32,有了segmentShiftConcurrentHashMap是如何做第一次哈希的呢?

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    // 变量j代表着数据项处于segment数组中的第j项
    int j = (hash >>> segmentShift) & segmentMask;
        // 如果segment[j]为null,则下面的这个方法负责初始化之
        s = ensureSegment(j); 
    return s.put(key, hash, value, false);
}

我们以put方法为例,变量j代表着数据项处于segment数组中的第j项。如下图所示假如segment数组的大小为2的n次方,则hash >>> segmentShift正好取了key的哈希值的高n位,再与掩码segmentMask相与相当与仍然用key的哈希的高位来确定数据项在segment数组中的位置。

image-20210409232020703

hash方法与非线程安全的HashMap相似,这里不再细说。

细节三:

在延迟初始化Segment数组时,作者采用了CAS避免了加锁,而且CAS可以保证最终的初始化只能被一个线程完成。在最终决定调用CAS进行初始化前又做了两次检查,第一次检查可以避免重复初始化tab数组,而第二次检查则可以避免重复初始化Segment对象,每一行代码作者都有详细的考虑。

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset 实际的字节偏移量
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck 再检查一次是否已经被初始化
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) // 使用 CAS 确保只被初始化一次
                    break;
            }
        }
    }
    return seg;
}

put方法

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); 
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k; // 如果找到key相同的数据项,则直接替换
                if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount; 
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    // node不为空说明已经在自旋等待时初始化了,注意调用的是setNext,不是直接操作next
                    node.setNext(first); 
                else
                    // 否则,在这里新建一个HashEntry
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1; // 先加1
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    // 将新节点写入,注意这里调用的方法有门道
                    setEntryAt(tab, index, node); 
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

这段代码在整个ConcurrentHashMap的设计中非常出彩,在这短短的40行代码中,Doug Lea就像一位神奇的魔术师,转眼间已经变换了好几种魔法,让人目瞪口呆,感叹其对并发的理解之深,让我们慢慢的解析Doug Lea在这段代码中使用的魔法:

细节四:

CPU的调度是公平的,好不容易轮到的时间片如果因为获取不到锁就将本线程挂起无疑会降低本线程的效率,更何况挂起之后还要重新调度,切换上下文,又是一笔不小的开销。如果可以遇见其他线程占有锁的时间不会很长,采用自旋将会是一个比较好的选择,在这里面也有一个权衡,如果别的线程占有锁的时间过长,反而是挂起阻塞等待性能好一点,我们来看下ConcurrentHashMap的做法:

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) { // 自旋等待
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) { // 这个桶中还没有写入k-v项
                if (node == null) // speculatively create node 直接创建一个新的节点
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;  
            }
            // key值相等,直接跳出去尝试获取锁
            else if (key.equals(e.key))
                retries = 0;
            else // 遍历链表
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {
            // 自旋等待超过一定次数之后只能挂起线程,阻塞等待了
            lock();
            break;
        }
        else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { 
            // 如果头节点改变了,则重置次数,继续自旋等待
            e = first = f; 
            retries = -1; 
        }
    }
    return node;
}

ConcurrentHashMap的策略是自旋MAX_SCAN_RETRIES次,如果还没有获取到锁则调用lock挂起阻塞等待,当然如果其他线程采用头插法改变了链表的头结点,则重置自旋等待次数。

细节五:

要知道,如果要从编码的角度提升系统的并发度,一个黄金法则就是减少并发临界区的大小。在scanAndLockForPut这个方法的设计上,有个小细节让我眼前一亮,就是在自旋的过程中初始化了一个HashEntry,这样做的好处就是线程在拿到锁之后不用初始化HashEntry了,占有锁的时间相应减小,进而提升性能。

细节六:

put方法的开头,有这么一行不起眼的代码:

HashEntry<K,V>[] tab = table;

看起来好像就是简单的临时变量赋值,其实大有来头,我们看一下table的声明:

transient volatile HashEntry<K,V>[] table;

table变量被关键字volatile修饰,CPU在处理volatile修饰的变量的时候会有下面的行为:

嗅探

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里

因此直接读取这类变量的读取和写入比普通变量的性能消耗更大,因此在put方法的开头将table变量赋值给一个普通的本地变量目的是为了消除volatile带来的性能损耗。这里就有另外一个问题:那这样做会不会导致table的语义改变,让别的线程读取不到最新的值呢?别着急,我们接着看。

细节七:

注意put方法中的这个方法:entryAt():

static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
    return (tab == null) ? null : (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)i << TSHIFT) + TBASE);
}

这个方法的底层会调用UNSAFE.getObjectVolatile,这个方法的目的就是对于普通变量读取也能像volatile修饰的变量那样读取到最新的值,在前文中我们分析过,由于变量tab现在是一个普通的临时变量,如果直接调用tab[i]不一定能拿到最新的首节点的。细心的读者读到这里可能会想:Doug Lea是不是糊涂了,兜兜转换不是回到了原点么,为啥不刚开始就操作volatile变量呢,费了这老大劲。我们继续往下看。

细节八:

put方法的实现中,如果链表中没有key值相等的数据项,则会把新的数据项插入到链表头写入到数组中,其中调用的方法是:

static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i, HashEntry<K,V> e) {
    UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}

putOrderedObject这个接口写入的数据不会马上被其他线程获取到,而是在put方法最后调用unclock后才会对其他线程可见,参见前文中对JMM的描述:

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

这样的好处有两个,第一是性能,因为在持有锁的临界区不需要有同步主存的操作,因此持有锁的时间更短。第二是保证了数据的一致性,在put操作的finally语句执行完之前,put新增的数据是不对其他线程展示的,这是ConcurrentHashMap实现无锁读的关键原因。

我们在这里稍微总结一下put方法里面最重要的三个细节,首先将volatile变量转为普通变量提升性能,因为在put中需要读取到最新的数据,因此接下来调用UNSAFE.getObjectVolatile获取到最新的头结点,但是通过调用UNSAFE.putOrderedObject让变量写入主存的时间延迟到put方法的结尾,一来缩小临界区提升性能,而来也能保证其他线程读取到的是完整数据。

细节九:

如果put真的需要往链表头插入数据项,那也得注意了,ConcurrentHashMap相应的语句是:

node.setNext(first);

我们看下setNext的具体实现:

final void setNext(HashEntry<K,V> n) {
    UNSAFE.putOrderedObject(this, nextOffset, n);
}

因为next变量是用volatile关键字修饰的,这里调用UNSAFE.putOrderedObject相当于是改变了volatile的语义,这里面的考量有两个,第一个仍然是性能,这样的实现性能明显更高,这一点前文已经详细的分析过,第二点是考虑了语义的一致性,对于put方法来说因为其调用的是UNSAFE.getObjectVolatile,仍然能获取到最新的数据,对于get方法,在put方法未结束之前,是不希望不完整的数据被其他线程通过get方法读取的,这也是合理的。

resize扩容

private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null) //  Single node on list 只有一个节点,简单处理
                newTable[idx] = e;
            else { 
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                // 保证下文中newTable[k]不会为null
                for (HashEntry<K,V> last = next;
                        last != null;
                        last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes 对标记之前的不能重用的节点进行复制,再重新添加到新数组对应的hash桶中去
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node 部分的put功能,把新节点添加到链表的最前面
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

如果大家看过我们上一篇分析HashMaprehash的过程看这段代码就会比较轻松,在上一篇我们分析过,在整个桶数组长度为2的正整数幂的情况下,扩容前同一个桶中的元素在扩容后只会分布在两个桶中,其中一个桶的下标保持不变,我们称之为旧桶,另一个桶的下标为旧桶下标加上旧的容量,我们称之为新桶,其实第一个for循环的目的就是在一个链表中找到最后一个应该移到新桶的数据项,直接移到新桶中,这样做是为了保证后面调用HashEntry<K,V> n = newTable[k];的时候不会读取到null。第二个for就比较简单了,将所有的数据项移到新的桶数组中,当所有的操作完成之后才将newTable赋值给table

rehash方法中是没有加锁的,并不是说调用这个方法不需要加锁,作者是在外层加了锁,这一点需要注意。

size方法

之前在分析HashMap方法的时候我们并没有去讲size方法,因为在单线程环境下这个方法可以使用一个全局的变量解决,同样的方案当然也可以在多线程场景下使用,不过要在多线程环境下读取全局变量又会陷入到无尽的“锁”中,这是我们不愿意看到的,那ConcurrentHashMap是如何解决这个问题的呢:

public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

在前面介绍put方法时我们选择忽略了一个小小的成员变量modCount,这个变量在这里大显身手,它的主要作用就是记录整个Segment中写入操作的次数,因为写入操作是会影响整个ConcurrentHashMap的大小的。

因为在读取ConcurrentHashMap大小的时候需要保证读到的是最新的值,因此其调用了UNSAFE.getObjectVolatile这个方法,虽然这个方法的性能比普通变量要差,但是比起全局加锁,可好多了。

static final <K,V> Segment<K,V> segmentAt(Segment<K,V>[] ss, int j) {
    long u = (j << SSHIFT) + SBASE; // 计算实际的字节偏移量
    return ss == null ? null : (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u);
}

细节十:

size方法的设计上,ConcurrentHashMap先尝试无锁的方法,如果两次遍历所有segment数组的时候整个ConcurrentHashMap没有发生写入操作,则直接返回每个segment数组的size()之和,否则重新遍历,如果写入操作频繁,则不得已加锁处理,这里的加锁相当于是一个全局的锁,因为对segment数组的每一个元素都加了锁。那如何判断整个ConcurrentHashMap的写入是否频繁呢?就看无锁重试的次数,当无锁重试的次数超过阈值的话就全局加锁处理。

总结

在看完ConcurrentHashMap中的这些细节之后我们尝试回答一下文章开头提出来的问题:

  1. ConcurrentHashMap的哪些操作需要加锁?

    答:只有写入操作才需要加锁,读取操作不需要加锁

  2. ConcurrentHashMap的无锁读是如何实现的?

    答:首先HashEntry中的valuenext都是有volatile修饰的,其次在写入操作的时候通过调用UNSAFE库延迟同步了主存,保证了数据的一致性

  3. 在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?

    答:size()具有全局的语义,如何能保证在不加全局锁的情况下读取到全局状态的值是一个很大的挑战,ConcurrentHashMap通过查看两次无锁读中间是否发生了写入操作来决定读取到的size()是否可信,如果写入操作频繁,则再退化为全局加锁读取。

  4. 在有Segment存在的前提下,是如何扩容的?

    答:segment数组的大小在一开始初始化的时候就已经决定了,扩容主要扩的是HashEntry数组,基本的思路与HashTable一致,但这是一个线程不安全方法,调用之前需要加锁。

- EOF -

PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。

大家一起在评论区聊聊呗~

茉莉花,别名茉莉,拉丁文名:Jasminum sambac (L.) Ait,木犀科、素馨属直立或攀援灌木,高达3米。小枝圆柱形或稍压扁状,有时中空,疏被柔毛。叶对生,单叶,叶片纸质,圆形、椭圆形、卵状椭圆形或倒卵形,两端圆或钝,基部有时微心形,在上面稍凹入或凹起,下面凸起,细脉在两面常明显,微凸起,除下面脉腋间常具簇毛外,其余无毛;裂片长圆形至近圆形,先端圆或钝。果球形,呈紫黑色。花期5-8月,果期7-9月。茉莉的花极香,为著名的花茶原料及重要的香精原料;花、叶药用治目赤肿痛,并有止咳化痰之效。吴王子子驹亡走闽越,怨东瓯杀其父,常劝闽越击东瓯。至建元三年,闽越发兵围东瓯。东瓯食尽,困,太史公曰:余每读虞书,至於君臣相敕,维是几安,而股肱不良,万事堕坏,未尝不流涕也。成王作颂,推己惩艾,悲彼家难,可不谓战战恐惧,善守善终哉?君子不为约则修德,满则弃礼,佚能思初,安能惟始,沐浴膏泽而歌咏勤苦,非大德谁能如斯!传曰“治定功成,礼乐乃兴”。海内人道益深,其德益至,所乐者益异。满而不损则溢,盈而不持则倾。凡作乐者,所以节乐。君子以谦退为礼,以损减为乐,乐其如此也。以为州异国殊,情习不同,故博采风俗,协比声律,以补短移化,助流政教。天子躬於明堂临观,而万民咸荡涤邪秽,斟酌饱满,以饰厥性。故云雅颂之音理而民正,嘄噭之声兴而士奋,郑卫之曲动而心淫。及其调和谐合,鸟兽尽感,而况怀五常,含好恶,自然之势也?  治道亏缺而郑音兴起,封君世辟,名显邻州,争以相高。自仲尼不能与齐优遂容於鲁,虽退正乐以诱世,作五章以剌时,犹莫之化。陵迟以至六国,流沔沈佚,遂往不返,卒於丧身灭宗,并国於秦。  秦二世尤以为娱。丞相李斯进谏曰:“放弃诗书,极意声色,祖伊所以惧也;轻积细过,恣心长夜,纣所以亡也。”赵高曰:“五帝、三王乐各殊名,示不相袭。上自朝廷,下至人民,得以接欢喜,合殷勤,非此和说不通,解泽不流,亦各一世之化,度时之乐,何必华山之騄耳而后行远乎?”二世然之。  高祖过沛诗三侯之章,令小兒歌之。高祖崩,令沛得以四时歌鳷宗庙。孝惠、孝文、孝景无所增更,於乐府习常肄旧而已。  至今上即位,作十九章,令侍中李延年次序其声,拜为协律都尉。通一经之士不能独知其辞,皆集会五经家,相与共讲习读之,乃能通知其意,多尔雅之文。  汉家常以正月上辛祠太一甘泉,以昏时夜祠,到明而终。常有流星经於祠坛上。使僮男僮女七十人俱歌。春歌青阳,夏歌硃明,秋歌西昚,冬歌玄冥。世多有,故不论。  又尝得神马渥洼水中,复次以为太一之歌。曲曰:“太一贡兮天马下,霑赤汗兮沫流赭。骋容与兮跇万里,今安匹兮龙为友。”後伐大宛得千里马,马名蒲梢,次作以为歌。歌诗曰:“天马来兮从西极,经万里兮归有德。承灵威兮降外国,涉流沙兮四夷服。”中尉汲黯进曰:“凡王者作乐,上以承祖宗,下以化兆民。今陛下得马,诗以为歌,协於宗庙,先帝百姓岂能知其音邪?”上默然不说。丞相公孙弘曰:“黯诽谤圣制,当族。”  凡音之起,由人心生也。人心之动,物使之然也。感於物而动,故形於声;声相应,故生变;变成方,谓之音;比音而乐之,及干戚羽旄,谓之乐也。乐者,音之所由生也,其本在人心感於物也。是故其哀心感者,其声噍以杀;其乐心感者,其声啴以缓;其喜心感者,其声发以散;其怒心感者,其声粗以厉;其敬心感者,其声直以廉;其爱心感者,其声和以柔。六者非性也,感於物而后动,是故先王慎所以感之。故礼以导其志,乐以和其声,政以壹其行,刑以防其奸。礼乐刑政,其极一也,所以同民心而出治道也。  凡音者,生人心者也。情动於中,故形於声,声成文谓之音。是故治世之音安以乐,其正和;乱世之音怨以怒,其正乖;亡国之音哀以思,其民困。声音之道,与正通矣。宫为君,商为臣,角为民,徵为事,羽为物。五者不乱,则无怗懘之音矣。宫乱则荒,其君骄;商乱则搥,其臣坏;角乱则忧,其民怨;徵乱则哀,其事勤;羽乱则危,其财匮。五者皆乱,迭相陵,谓之慢。如此则国之灭亡无日矣。郑卫之音,乱世之音也,比於慢矣。桑间濮上之音,亡国之音也,其政散,其民流,诬上行私而不可止。  凡音者,生於人心者也;乐者,通於伦理者也。是故知声而不知音者,禽兽是也;知音而不知乐者,众庶是也。唯君子为能知乐。是故审声以知音,审音以知乐,审乐以知政,而治道备矣。是故不知声者不可与言音,不知音者不可与言乐知乐则几於礼矣。礼乐皆得,谓之有德。德者得也。是故乐之隆,非极音也;食飨之礼,非极味也。清庙之瑟,硃弦而疏越,一倡而三叹,有遗音者矣。大飨之礼,尚玄酒而俎腥鱼,大羹不和,有遗味者矣。是故先王之制礼乐也,非以极口腹耳目之欲也,将以教民平好恶而反人道之正也。  人生而静,天之性也;感於物而动,性之颂也。物至知知,然后好恶形焉。好恶无节於内,知诱於外,不能反己,天理灭矣。夫物之感人无穷,而人之好恶无节,则是物至而人化物也。人化物也者,灭天理而穷人欲者也。於是有悖逆诈伪之心,有淫佚作乱之事。是故彊者胁弱,众者暴寡,知者诈愚,勇者苦怯,疾病不养,老幼孤寡不得其所,此大乱之道也。是故先王制礼乐,人为之节:衰麻哭泣,所以节丧纪也;钟鼓干戚,所以和安乐也;婚姻冠笄,所以别男女也;射乡食飨,所以正交接也。礼节民心,乐和民声,政以行之,刑以防之。礼乐刑政四达而不悖,则王道备矣。  乐者为同,礼者为异。同则相亲,异则相敬。乐胜则流,礼胜则离。合情饰貌者,礼乐之事也。礼义立,则贵贱等矣;乐文同,则上下和矣;好恶著,则贤不肖别矣;刑禁暴,爵举贤,则政均矣。仁以爱之,义以正之,如此则民治行矣。  乐由中出,礼自外作。乐由中出,故静;礼自外作,故文。大乐必易,大礼必简。乐至则无怨,礼至则不争。揖让而治天下者,礼乐之谓也。暴民不作,诸侯宾服,兵革不试,五刑不用,百姓无患,天子不怒,如此则乐达矣。合父子之亲,明长幼之序,以敬四海之内。天子如此,则礼行矣。  大乐与天地同和,大礼与天地同节。和,故百物不失;节,故祀天祭地。明则有礼乐,幽则有鬼神,如此则四海之内合敬同爱矣。礼者,殊事合敬者也;乐者,异文合爱者也。礼乐之情同,故明王以相沿万石君名奋,其父赵人也,姓石氏。赵亡,徙居温。高祖东击项籍,过河内,时奋年十五,为小吏,侍高祖。高祖与语,爱其恭敬,问曰:“若何有?”对曰:“奋独有母,不幸失明。家贫。有姊,能鼓琴。”高祖曰:“若能从我乎?”曰:“原尽力。”於是高祖召其姊为美人,以奋为中涓,受书谒,徙其家长安中戚里,以姊为美人故也。其官至孝文时,积功劳至大中大夫。无文学,恭谨无与比。  文帝时,东阳侯张相如为太子太傅,免。选可为傅者,皆推奋,奋为太子太傅。及孝景即位,以为九卿;迫近,惮之,徙奋为诸侯相。奋长子建,次子甲,次子乙,次子庆,皆以驯行孝谨,官皆至二千石。於是景帝曰:“石君及四子皆二千石,人臣尊宠乃集其门。”号奋为万石君。  孝景帝季年,万石君以上大夫禄归老于家,以岁时为朝臣。过宫门阙,万石君必下车趋,见路马必式焉。子孙为小吏,来归谒,万石君必朝服见之,不名。子孙有过失,不谯让,为便坐,对案不食。然后诸子相责,因长老肉袒固谢罪,改之,乃许。子孙胜冠者在侧,虽燕居必冠,申申如也。僮仆如也,唯谨。上时赐食於家,必稽首俯伏而食之,如在上前。其执丧,哀戚甚悼。子孙遵教,亦如之。万石君家以孝谨闻乎郡国,虽齐鲁诸儒质行,皆自以为不及也。  建元二年,郎中令王臧以文学获罪。皇太后以为儒者文多质少,今万石君家不言而躬行,乃以长子建为郎中令,少子庆为内史。  建老白首,万石君尚无恙。建为郎中令,每五日洗沐归谒亲,入子舍,窃问侍者,取亲中稖厕窬,身自浣涤,复与侍者,不敢令万石君知,以为常。建为郎中令,事有可言,屏人恣言,极切;至廷见,如不能言者。是以上乃亲尊礼之。  万石君徙居陵里。内史庆醉归,入外门不下车。万石君闻之,不食。庆恐,肉袒请罪,不许。举宗及兄建肉袒,万石君让曰:“内史贵人,入闾里,里中长老皆走匿,而内史坐车中自如,固当!”乃谢罢庆。庆及诸子弟入里门,趋至家。  万石君以元朔五年中卒。长子郎中令建哭泣哀思,扶杖乃能行。岁馀,建亦死。诸子孙咸孝,然建最甚,甚於万石君。  建为郎中令,书奏事,事下,建读之,曰:“误书!‘马’者与尾当五,今乃四,不足一。上谴死矣!”甚惶恐。其为谨慎,虽他皆如是。  万石君少子庆为太仆,御出,上问车中几马,庆以策数马毕,举手曰:“六马。”庆於诸子中最为简易矣,然犹如此。为齐相,举齐国皆慕其家行,不言而齐国大治,为立石相祠。  元狩元年,上立太子,选群臣可为傅者,庆自沛守为太子太傅,七岁迁为御史大夫。  元鼎五年秋,丞相有罪,罢。制诏御史:“万石君先帝尊之,子孙孝,其以御史大夫庆为丞相,封为牧丘侯。”是时汉方南诛两越,东击朝鲜,北逐匈奴,西伐大宛,中国多事。天子巡狩海内,修上古神祠,封禅,兴礼乐。公家用少,桑弘羊等致利,王温舒之属峻法,兒宽等推文学至九卿,更进用事,事不关决於丞相,丞相醇谨而已。在位九岁,无能有所匡言。尝欲请治上近臣所忠、九卿咸宣罪,不能服,反受其过,赎罪。  元封四年中,关东流民二百万口,无名数者四十万,公卿议欲请徙流民於边以適之。上以为丞相老谨,不能与其议,乃赐丞相告归,而案御史大夫以下议为请者。丞相惭不任职,乃上书曰:“庆幸得待罪丞相,罢驽无以辅治,城郭仓库空虚,民多流亡,罪当伏斧质,上不忍致法。原归丞相侯印,乞骸骨归,避贤者路。”天子曰:“仓廪既空,民贫流亡,而君欲请徙之,摇荡不安,动危之,而辞位,君欲安归难乎?”以书让庆,庆甚惭,遂复视事。  庆文深审谨,然无他大略,为百姓言。後三岁馀,太初二年中,丞相庆卒,谥为恬侯。庆中子德,庆爱用之,上以德为嗣,代侯。後为太常,坐法当死,赎免为庶人。庆方为丞相,诸子孙为吏更至二千石者十三人。及庆死後,稍以罪去,孝谨益衰矣。  建陵侯卫绾者,代大陵人也。绾以戏车为郎,事文帝,功次迁为中郎将,醇谨无他。孝景为太子时,召上左右饮,而绾称病不行。文帝且崩时,属孝景曰:“绾长者,善遇之。”及文帝崩,景帝立,岁馀不噍呵绾,绾日以谨力。  景帝幸上林,诏中郎将参乘,还而问曰:“君知所以得参乘乎?”绾曰:“臣从车士幸得以功次迁为中郎将,不自知也。”上问曰:“吾为太子时召君,君不肯来,何也?”对曰:“死罪,实病!”上赐之剑。绾曰:“先帝赐臣剑凡六,剑不敢奉诏。”上曰:“剑,人之所施易,独至今乎?”绾曰:“具在。”上使取六剑,剑尚盛,未尝服也。郎官有谴,常蒙其罪,不与他将争;有功,常让他将。上以为廉,忠实无他肠,乃拜绾为河间王太傅。吴楚反,诏绾为将,将河间兵击吴楚有功,拜为中尉。三岁,以军功,孝景前六年中封绾为建陵侯。  其明年,上废太子,诛栗卿之属。上以为绾长者,不忍,乃赐绾告归,而使郅都治捕栗氏。既已,上立胶东王为太子,召绾,拜为太子太傅。久之,迁为御史大夫。五岁,代桃侯舍为丞相,朝奏事如职所奏。然自初官以至丞相,终无可言。天子以为敦厚,可相少主,尊宠之,赏赐甚多。  为丞相三岁,景帝崩,武帝立。建元年中,丞相以景帝疾时诸官囚多坐不辜者,而君不任职,免之。其後绾卒,子信代。坐酎金失侯。  塞侯直不疑者,南阳人也。为郎,事文帝。其同舍有告归,误持同舍郎金去,已而金主觉,妄意不疑,不疑谢有之,买金偿。而告归者来而归金,而前郎亡金者大惭,以此称为长者。文帝称举,稍迁至太中大夫。朝廷见,人或毁曰:“不疑状貌甚美,然独无柰其善盗嫂何也!”不疑闻,曰:“我乃无兄。”然终不自明也。  吴楚反时,不疑以二千石将兵击之。景帝後元年,拜为御史大夫。天子修吴楚时功,乃封不疑为塞侯。武帝建元年中,谚曰“力田不如逢年,善仕不如遇合”,固无虚言。非独女以色媚,而士宦亦有之。  昔以色幸者多矣。至汉兴,高祖至暴抗也,然籍孺以佞幸;孝惠时有闳孺。此两人非有材能,徒以婉佞贵幸,与上卧起,公卿皆因关说。故孝惠时郎侍中皆冠鵕璘,贝带,傅脂粉,化闳、籍之属也。两人徙家安陵。  孝文时中宠臣,士人则邓通,宦者则赵同、北宫伯子。北宫伯子以爱人长者;而赵同以星气幸,常为文帝参乘;邓通无伎能。邓通,蜀郡南安人也,以濯船为黄头郎。孝文帝梦欲上天,不能,有一黄头郎从後推之上天,顾见其衣裻带後穿。觉而之渐台,以梦中阴目求推者郎,即见邓通,其衣後穿,梦中所见也。召问其名姓,姓邓氏,名通,文帝说焉,尊幸之日异。通亦愿谨,不好外交,虽赐洗沐,不欲出。於是文帝赏赐通巨万以十数,官至上大夫。文帝时时如邓通家游戏。然邓通无他能,不能有所荐士,独自谨其身以媚上而已。上使善相者相通,曰“当贫饿死”。文帝曰:“能富通者在我也。何谓贫乎?”於是赐邓通蜀严道铜山,得自铸钱,“邓氏钱”布天下。其富如此。  文帝尝病痈,邓通常为帝唶吮之。文帝不乐,从容问通曰:“天下谁最爱我者乎?”通曰:“宜莫如太子。”太子入问病,文帝使唶痈,唶痈而色难之。已而闻邓通常为帝唶吮之,心惭,由此怨通矣。及文帝崩,景帝立,邓通免,家居。居无何,人有告邓通盗出徼外铸钱。下吏验问,颇有之,遂竟案,尽没入邓通家,尚负责数巨万。长公主赐邓通,吏辄随没入之,一簪不得著身。於是长公主乃令假衣食。竟不得名一钱,寄死人家。  孝景帝时,中无宠臣,然独郎中令周文仁,仁宠最过庸,乃不甚笃。  今天子中宠臣,士人则韩王孙嫣,宦者则李延年。嫣者,弓高侯孽孙也。今上为胶东王时,嫣与上学书相爱。及上为太子,愈益亲嫣。嫣善骑射,善佞。上即位,欲事伐匈奴,而嫣先习胡兵,以故益尊贵,官至上大夫,赏赐拟於邓通。时嫣常与上卧起。江都王入朝,有诏得从入猎上林中。天子车驾跸道未行,而先使嫣乘副车,从数十百骑,骛驰视兽。江都王望见,以为天子,辟从者,伏谒道傍。嫣驱不见。既过,江都王怒,为皇太后泣曰:“请得归国入宿卫,比韩嫣。”太后由此嗛嫣。嫣侍上,出入永巷不禁,以奸闻皇太后。皇太后怒,使使赐嫣死。上为谢,终不能得,嫣遂死。而案道侯韩说,其弟也,亦佞幸。  李延年,中山人也。父母及身兄弟及女,皆故倡也。延年坐法腐,给事狗中。而平阳公主言延年女弟善舞,上见,心说之,及入永巷,而召贵延年。延年善歌,为变新声,而上方兴天地祠,欲造乐诗歌弦之。延年善承意,弦次初诗。其女弟亦幸,有子男。延年佩二千石印,号协声律。与上卧起,甚贵幸,埒如韩嫣也。久之,浸与中人乱,出入骄恣。及其女弟李夫人卒後,爱弛,则禽诛延年昆弟也。  自是之後,内宠嬖臣大底外戚之家,然不足数也。卫青、霍去病亦以外戚贵幸,然颇用材能自进。  太史公曰:甚哉爱憎之时!弥子瑕之行,足以观後人佞幸矣。虽百世可知也。  传称令色,诗刺巧言。冠璘入侍,傅粉承恩。黄头赐蜀,宦者同轩。新声都尉,挟弹王孙。泣鱼窃驾,著自前论。与丞相绾俱以过免。
  不疑学老子言。其所临,为官如故,唯恐人知其为吏迹也。不好立名称,称为长者。不疑卒,子相如代。孙望,坐酎金失侯。  郎中令周文者,名仁,其先故任城人也。以医见。景帝为太子时,拜为舍人,积功稍迁,孝文帝时至太中大夫。景帝初即位,拜仁为郎中令。  仁为人阴重不泄,常衣敝补衣溺袴,期为不絜清,以是得幸。景帝入卧内,於後宫祕戏,仁常在旁。至景帝崩,仁尚为郎中令,终无所言。上时问人,仁曰:“上自察之。”然亦无所毁。以此景帝再自幸其家。家徙阳陵。上所赐甚多,然常让,不敢受也。诸侯群臣赂遗,终无所受。  武帝立,以为先帝臣,重之。仁乃病免,以二千石禄归老,子孙咸至大官矣。  御史大夫张叔者,名欧,安丘侯说之庶子也。孝文时以治刑名言事太子。然欧虽治刑名家,其人长者。景帝时尊重,常为九卿。至武帝元朔四年,韩安国免,诏拜欧为御史大夫。自欧为吏,未尝言案人,专以诚长者处官。官属以为长者,亦不敢大欺。上具狱事,有可卻,卻之;不可者,不得已,为涕泣面对而封之。其爱人如此。  老病笃,请免。於是天子亦策罢,以上大夫禄归老于家。家於阳陵。子孙咸至大官矣。  太史公曰:仲尼有言曰“君子欲讷於言而敏於行”,其万石、建陵、张叔之谓邪?是以其教不肃而成,不严而治。塞侯微巧,而周文处讇,君子讥之,为其近於佞也。然斯可谓笃行君子矣!  万石孝谨,自家形国。郎中数马,内史匍匐。绾无他肠,塞有阴德。刑名张欧,垂涕恤狱。敏行讷言,俱嗣芳躅。也。故事与时并,名与功偕。故钟鼓管磬羽籥干戚,乐之器也;诎信俯仰级兆舒疾,乐之文也。簠簋俎豆制度文章,礼之器也;升降上下周旋裼袭,礼之文也。故知礼乐之情者能作,识礼乐之文者能术。作者之谓圣,术者之谓明。明圣者,术作之谓也。
  乐者,天地之和也;礼者,天地之序也。和,故百物皆化;序,故群物皆别。乐由天作,礼以地制。过制则乱,过作则暴。明於天地,然後能兴礼乐也。论伦无患,乐之情也;欣喜驩爱,乐之也。中正无邪,礼之质也;庄敬恭顺,礼之制也。若夫礼乐之施於金石,越於声音,用於宗庙社稷,事于山川鬼神,则此所以与民同也。  王者功成作乐,治定制礼。其功大者其乐备,其治辨者其礼具。干戚之舞,非备乐也;亨孰而祀,非达礼也。五帝殊时,不相沿乐;三王异世,不相袭礼。乐极则忧,礼粗则偏矣。及夫敦乐而无忧,礼备而不偏者,其唯大圣乎?天高地下,万物散殊,而礼制行也;流而不息,合同而化,而乐兴也。春作夏长,仁也;秋敛冬藏,义也。仁近於乐,义近於礼。乐者敦和,率神而从天;礼者辨宜,居鬼而从地。故圣人作乐以应天,作礼以配地。礼乐明备,天地官矣。  天尊地卑,君臣定矣。高卑已陈,贵贱位矣。动静有常,小大殊矣。方以类聚,物以群分,则性命不同矣。在天成象,在地成形,如此则礼者天地之别也。地气上隮,天气下降,阴阳相摩,天地相荡,鼓之以雷霆,奋之以风雨,动之以四时,暖之以日月,而百化兴焉,如此则乐者天地之和也。  化不时则不生,男女无别则乱登,此天地之情也。及夫礼乐之极乎天而蟠乎地,行乎阴阳而通乎鬼神,穷高极远而测深厚,乐著太始而礼居成物。著不息者天也,著不动者地也。一动一静者,天地之间也。故圣人曰“礼云乐云”。且降,乃使人告急天子。天子问太尉田蚡,蚡对曰:“越人相攻击,固其常,又数反覆,不足以烦中国往救也。自秦时弃弗属。”於是中大夫庄助诘蚡曰:“特患力弗能救,德弗能覆;诚能,何故弃之?且秦举咸阳而弃之,何乃越也!今小国以穷困来告急天子,天子弗振,彼当安所告愬?又何以子万国乎?”上曰:“太尉未足与计。吾初即位,不欲出虎符发兵郡国。”乃遣庄助以节发兵会稽。会稽太守欲距不为发兵,助乃斩一司马,谕意指,遂发兵浮海救东瓯。未至,闽越引兵而去。东瓯请举国徙中国,乃悉举众来,处江淮之间。
  至建元六年,闽越击南越。南越守天子约,不敢擅发兵击而以闻。上遣大行王恢出豫章,大农韩安国出会稽,皆为将军。兵未逾岭,闽越王郢发兵距险。其弟馀善乃与相、宗族谋曰:“王以擅发兵击南越,不请,故天子兵来诛。今汉兵众彊,今即幸胜之,後来益多,终灭国而止。今杀王以谢天子。天子听,罢兵,固一国完;不听,乃力战;不胜,即亡入海。”皆曰“善”。即鏦杀王,使使奉其头致大行。大行曰:“所为来者诛王。今王头至,谢罪,不战而耘,利莫大焉。”乃以便宜案兵告大农军,而使使奉王头驰报天子。诏罢两将兵,曰:“郢等首恶,独无诸孙繇君丑不与谋焉。”乃使郎中将立丑为越繇王,奉闽越先祭祀。  馀善已杀郢,威行於国,国民多属,窃自立为王。繇王不能矫其众持正。天子闻之,为馀善不足复兴师,曰:“馀善数与郢谋乱,而後首诛郢,师得不劳。”因立馀善为东越王,与繇王并处。  至元鼎五年,南越反,东越王馀善上书,请以卒八千人从楼船将军击吕嘉等。兵至揭扬,以海风波为解,不行,持两端,阴使南越。及汉破番禺,不至。是时楼船将军杨仆使使上书,原便引兵击东越。上曰士卒劳倦,不许,罢兵,1、伯庸。《离骚》:“朕皇考曰伯庸”。譬如作家马伯庸……
2、正则、灵均。《离骚》:“名余曰正则兮,字余曰灵均”。正则:公正而有法则。灵均:灵善而均调。屈原名平,字原,正则是对“平”字进行的解释,灵均是对“原”字进行的解释。
3、修能。《离骚》:“又重之以修能”。修能:即美好的外表仪形。一释为很强的才干和能力。
4、骐、骥。《离骚》:“乘骐骥以驰骋兮”。骐骥:骏马。
5、峻茂。《离骚》:“冀枝叶之峻茂兮”。风信子(学名:Hyacinthus orientalis L.):是多年草本球根类植物,鳞茎卵形,有膜质外皮,皮膜颜色与花色成正相关,未开花时形如大蒜,原产地中海沿岸及小亚细亚一带,是研究发现的会开花的植物中最香的一个品种。喜阳光充足和比较湿润的生长环境,要求排水良好和肥沃的沙壤土等。全世界风信子的园艺品种约有单阏之岁兮,四月孟夏,庚子日施兮,服集予舍,止于坐隅,貌甚间暇。异物来集兮,私怪其故,发书占之兮,筴言其度。曰“野鸟入处兮,主人将去”。请问于服兮:“予去何之?吉乎告我,凶言其菑。淹数之度兮,语予其期。”服乃叹息,举首奋翼,口不能言,请对以意。  万物变化兮,固无休息。斡流而迁兮,或推而还。形气转续兮,变化而嬗。沕穆无穷兮,胡可胜言&#

以上是关于有十亿个数据如何取到最小的十个的主要内容,如果未能解决你的问题,请参考以下文章

ConcurrentHashMap中有十个提升性能的细节,你都知道吗?

本学期计划及其对构建之法的十个问题

2020年精心收集的十个Java开发网站

利用自定义View实现扫雷游戏

Neo4j 如何创建数十亿个节点?

ClassTwo__HomeWork