Shared posts

14 Jun 02:26

探索 ConcurrentHashMap 高并发性的实现机制

by importnewzz

简介

ConcurrentHashMap 是 util.concurrent 包的重要成员。本文将结合 Java 内存模型,分析 JDK 源代码,探索 ConcurrentHashMap 高并发的具体实现机制。

由于 ConcurrentHashMap 的源代码实现依赖于 Java 内存模型,所以阅读本文需要读者了解 Java 内存模型。同时,ConcurrentHashMap 的源代码会涉及到散列算法和链表数据结构,所以,读者需要对散列算法和基于链表的数据结构有所了解。

Java 内存模型

由于 ConcurrentHashMap 是建立在 Java 内存模型基础上的,为了更好的理解 ConcurrentHashMap,让我们首先来了解一下 Java 的内存模型。

Java 语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及何时可以确保它们对线程是可见的。下面我们将分别介绍 Java 内存模型的重排序,内存可见性和 happens-before 关系。

重排序

内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码 — 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

重排序类型包括:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
  • 处理器可以乱序或者并行的执行指令。
  • 缓存会改变写入提交到主内存的变量的次序。

内存可见性

由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。

在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:

  • 执行线程 A 的处理器把变量 V 缓存到寄存器中。
  • 执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。
  • 执行线程 B 的处理器的缓存中有变量 V 的旧值。

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。

下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。

ConcurrentHashMap 的结构分析

为了更好的理解 ConcurrentHashMap 高并发的具体实现,让我们先探索它的结构模型。

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

HashEntry 类

HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

清单 1.HashEntry 类的定义
static final class HashEntry<K,V> { 
        final K key;                       // 声明 key 为 final 型
        final int hash;                   // 声明 hash 值为 final 型 
        volatile V value;                 // 声明 value 为 volatile 型
        final HashEntry<K,V> next;      // 声明 next 为 final 型 

        HashEntry(K key, int hash, HashEntry<K,V> next, V value) { 
            this.key = key; 
            this.hash = hash; 
            this.next = next; 
            this.value = value; 
        } 
 }

在 ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。 下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图:

图 1. 插入三个节点后桶的结构示意图:

图 1. 插入三个节点后桶的结构示意图:

注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

Segment 类

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

清单 2.Segment 类的定义
static final class Segment<K,V> extends ReentrantLock implements Serializable { 
        /** 
         * 在本 segment 范围内,包含的 HashEntry 元素的个数
         * 该变量被声明为 volatile 型
         */ 
        transient volatile int count; 

        /** 
         * table 被更新的次数
         */ 
        transient int modCount; 

        /** 
         * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列
         */ 
        transient int threshold; 

        /** 
         * table 是由 HashEntry 对象组成的数组
         * 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表
         * table 数组的数组成员代表散列映射表的一个桶
         * 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
         * 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16 
         */ 
        transient volatile HashEntry<K,V>[] table; 

        /** 
         * 装载因子
         */ 
        final float loadFactor; 

        Segment(int initialCapacity, float lf) { 
            loadFactor = lf; 
            setTable(HashEntry.<K,V>newArray(initialCapacity)); 
        } 

        /** 
         * 设置 table 引用到这个新生成的 HashEntry 数组
         * 只能在持有锁或构造函数中调用本方法
         */ 
        void setTable(HashEntry<K,V>[] newTable) { 
            // 计算临界阀值为新数组的长度与装载因子的乘积
            threshold = (int)(newTable.length * loadFactor); 
            table = newTable; 
        } 

        /** 
         * 根据 key 的散列值,找到 table 中对应的那个桶(table 数组的某个数组成员)
         */ 
        HashEntry<K,V> getFirst(int hash) { 
            HashEntry<K,V>[] tab = table; 
            // 把散列值与 table 数组长度减 1 的值相“与”,
 // 得到散列值对应的 table 数组的下标
            // 然后返回 table 数组中此下标对应的 HashEntry 元素
            return tab[hash & (tab.length - 1)]; 
        } 
 }

下图是依次插入 ABC 三个 HashEntry 节点后,Segment 的结构示意图。

图 2. 插入三个节点后 Segment 的结构示意图:

图 2. 插入三个节点后 Segment 的结构示意图:

ConcurrentHashMap 类

ConcurrentHashMap 在默认并发级别会创建包含 16 个 Segment 对象的数组。每个 Segment 的成员对象 table 包含若干个散列表的桶。每个桶是由 HashEntry 链接起来的一个链表。如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16。

清单 3.ConcurrentHashMap 类的定义
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
        implements ConcurrentMap<K, V>, Serializable { 

    /** 
     * 散列映射表的默认初始容量为 16,即初始默认为 16 个桶
     * 在构造函数中没有指定这个参数时,使用本参数
     */ 
    static final 	 int DEFAULT_INITIAL_CAPACITY= 16; 

    /** 
     * 散列映射表的默认装载因子为 0.75,该值是 table 中包含的 HashEntry 元素的个数与
 * table 数组长度的比值
     * 当 table 中包含的 HashEntry 元素的个数超过了 table 数组的长度与装载因子的乘积时,
 * 将触发 再散列
     * 在构造函数中没有指定这个参数时,使用本参数
     */ 
    static final float DEFAULT_LOAD_FACTOR= 0.75f; 

    /** 
     * 散列表的默认并发级别为 16。该值表示当前更新线程的估计数
     * 在构造函数中没有指定这个参数时,使用本参数
     */ 
    static final int DEFAULT_CONCURRENCY_LEVEL= 16; 

    /** 
     * segments 的掩码值
     * key 的散列码的高位用来选择具体的 segment 
     */ 
    final int segmentMask; 

    /** 
     * 偏移量
     */ 
    final int segmentShift; 

    /** 
     * 由 Segment 对象组成的数组
     */ 
    final Segment<K,V>[] segments; 

    /** 
     * 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。
     */ 
    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; 

        // 寻找最佳匹配参数(不小于给定参数的最接近的 2 次幂) 
        int sshift = 0; 
        int ssize = 1; 
        while(ssize < concurrencyLevel) { 
            ++sshift; 
            ssize <<= 1; 
        } 
        segmentShift = 32 - sshift;       // 偏移量值
        segmentMask = ssize - 1;           // 掩码值 
        this.segments = Segment.newArray(ssize);   // 创建数组

        if (initialCapacity > MAXIMUM_CAPACITY) 
            initialCapacity = MAXIMUM_CAPACITY; 
        int c = initialCapacity / ssize; 
        if(c * ssize < initialCapacity) 
            ++c; 
        int cap = 1; 
        while(cap < c) 
            cap <<= 1; 

        // 依次遍历每个数组元素
        for(int i = 0; i < this.segments.length; ++i) 
            // 初始化每个数组元素引用的 Segment 对象
 this.segments[i] = new Segment<K,V>(cap, loadFactor); 
    } 

    /** 
     * 创建一个带有默认初始容量 (16)、默认加载因子 (0.75) 和 默认并发级别 (16) 
  * 的空散列映射表。
     */ 
    public ConcurrentHashMap() { 
        // 使用三个默认参数,调用上面重载的构造函数来创建空散列映射表
 this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); 
 }

下面是 ConcurrentHashMap 的结构示意图。

图 3.ConcurrentHashMap 的结构示意图:

图 3.ConcurrentHashMap 的结构示意图:

用分离锁实现多个线程间的并发写操作

在 ConcurrentHashMap 中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作才需要加锁。下面以 put 操作为例说明对 ConcurrentHashMap 做结构性修改的过程。

首先,根据 key 计算出对应的 hash 值:

清单 4.Put 方法的实现
public V put(K key, V value) { 
        if (value == null)          //ConcurrentHashMap 中不允许用 null 作为映射值
            throw new NullPointerException(); 
        int hash = hash(key.hashCode());        // 计算键对应的散列码
        // 根据散列码找到对应的 Segment 
        return segmentFor(hash).put(key, hash, value, false); 
 }

然后,根据 hash 值找到对应的Segment 对象:

清单 5.根据 hash 值找到对应的 Segment
/** 
     * 使用 key 的散列码来得到 segments 数组中对应的 Segment 
     */ 
 final Segment<K,V> segmentFor(int hash) { 
    // 将散列值右移 segmentShift 个位,并在高位填充 0 
    // 然后把得到的值与 segmentMask 相“与”
 // 从而得到 hash 值对应的 segments 数组的下标值
 // 最后根据下标值返回散列码对应的 Segment 对象
        return segments[(hash >>> segmentShift) & segmentMask]; 
 }

最后,在这个 Segment 中执行具体的 put 操作:

清单 6.在 Segment 中执行具体的 put 操作
V put(K key, int hash, V value, boolean onlyIfAbsent) { 
            lock();  // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap 
            try { 
                int c = count; 

                if (c++ > threshold)     // 如果超过再散列的阈值
                    rehash();              // 执行再散列,table 数组的长度将扩充一倍

                HashEntry<K,V>[] tab = table; 
                // 把散列码值与 table 数组的长度减 1 的值相“与”
                // 得到该散列码对应的 table 数组的下标值
                int index = hash & (tab.length - 1); 
                // 找到散列码对应的具体的那个桶
                HashEntry<K,V> first = tab[index]; 

                HashEntry<K,V> e = first; 
                while (e != null && (e.hash != hash || !key.equals(e.key))) 
                    e = e.next; 

                V oldValue; 
                if (e != null) {            // 如果键 / 值对以经存在
                    oldValue = e.value; 
                    if (!onlyIfAbsent) 
                        e.value = value;    // 设置 value 值
                } 
                else {                        // 键 / 值对不存在 
                    oldValue = null; 
                    ++modCount;         // 要添加新节点到链表中,所以 modCont 要加 1  
                    // 创建新节点,并添加到链表的头部 
                    tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                    count = c;               // 写 count 变量
                } 
                return oldValue; 
            } finally { 
                unlock();                     // 解锁
            } 
        }

注意:这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。

相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

用 HashEntery 对象的不变性来降低读操作对加锁的需求

在代码清单“HashEntry 类的定义”中我们可以看到,HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。

同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 unll 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。

下面我们分别来分析线程写入的两种情形:对散列表做非结构性修改的操作和对散列表做结构性修改的操作。

非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程“看到”。

对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。

结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。

clear 操作只是把 ConcurrentHashMap 中所有的桶“置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。

从上面的代码清单“在 Segment 中执行具体的 put 操作”中,我们可以看出:put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。

下面来分析 remove 操作,先让我们来看看 remove 操作的源代码实现。

清单 7.remove 操作
V remove(Object key, int hash, Object value) { 
            lock();         // 加锁
            try{ 
                int c = count - 1; 
                HashEntry<K,V>[] tab = table; 
                // 根据散列码找到 table 的下标值
                int index = hash & (tab.length - 1); 
                // 找到散列码对应的那个桶
                HashEntry<K,V> first = tab[index]; 
                HashEntry<K,V> e = first; 
                while(e != null&& (e.hash != hash || !key.equals(e.key))) 
                    e = e.next; 

                V oldValue = null; 
                if(e != null) { 
                    V v = e.value; 
                    if(value == null|| value.equals(v)) { // 找到要删除的节点
                        oldValue = v; 
                        ++modCount; 
                        // 所有处于待删除节点之后的节点原样保留在链表中
                        // 所有处于待删除节点之前的节点被克隆到新链表中
                        HashEntry<K,V> newFirst = e.next;// 待删节点的后继结点
                        for(HashEntry<K,V> p = first; p != e; p = p.next) 
                            newFirst = new HashEntry<K,V>(p.key, p.hash, 
                                                          newFirst, p.value); 
                        // 把桶链接到新的头结点
                        // 新的头结点是原链表中,删除节点之前的那个节点
                        tab[index] = newFirst; 
                        count = c;      // 写 count 变量
                    } 
                } 
                return oldValue; 
            } finally{ 
                unlock();               // 解锁
            } 
        }

和 get 操作一样,首先根据散列码找到具体的链表;然后遍历这个链表找到要删除的节点;最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明 remove 操作。假设写线程执行 remove 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。

图 4. 执行删除之前的原链表:

图 4. 执行删除之前的原链表:

图 5. 执行删除之后的新链表

图 5. 执行删除之后的新链表

从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了

在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。

综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。

用 Volatile 变量协调读写线程间的内存可见性

由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。

下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。

图 6. 协调读 – 写线程间的内存可见性的示意图:

图 6. 协调读 - 写线程间的内存可见性的示意图:

假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。

根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。

根据 Volatile 变量法则,B happens-before C。

根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。

虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能“看到”这些修改。

ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。

清单 8.Count 变量的声明
transient volatile int count;

所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法:

清单 9.get 操作
V get(Object key, int hash) { 
            if(count != 0) {       // 首先读 count 变量
                HashEntry<K,V> e = getFirst(hash); 
                while(e != null) { 
                    if(e.hash == hash && key.equals(e.key)) { 
                        V v = e.value; 
                        if(v != null)            
                            return v; 
                        // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取
                        return readValueUnderLock(e); 
                    } 
                    e = e.next; 
                } 
            } 
            return null; 
        }

在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。

根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程“看到”。

这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。

ConcurrentHashMap 实现高并发的总结

基于通常情形而优化

在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。

总结

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  1. 减小请求 同一个锁的 频率。
  2. 减少持有锁的 时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  1. 用分离锁实现多个线程间的更深层次的共享访问。
  2. 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  3. 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。

使用分离锁,减小了请求 同一个锁的频率。

通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。

通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。

相关文章

14 Jun 00:25

Linux运维工程师入门须掌握的10个技术点

by changqi

 本人是linux运维工程师,对这方面有点心得,现在我说说要掌握哪方面的工具吧

说到工具,在行外可以说是技能,在行内我们一般称为工具,就是运维必须要掌握的工具。

我就大概列出这几方面,这样入门就基本没问题了。

linux系统如果是学习可以选用redhat或centos,特别是centos在企业中用得最多,当然还会有其它版本的,但学习者还是以这2个版本学习就行,因为这两个版本都是兄弟,没区别的,有空可以再研究一下SUSE,有些公司也喜欢用,例如我公司 。。。。。

工具如下:

1、linux系统基础,这个不用说了,是基础中的基础,连这个都不会就别干了,参考书籍,可以看鸟哥linux基础篇,至少要掌握这书60%内容,没必须全部掌握,但基本命令总得会吧

2、网络服务,服务有很多种,每间公司都会用到不同的,但基础的服务肯定要掌握,如FTP, DNS,SAMBA, 邮件, 这几个大概学一下就行,LAMP和LNMP是必须要熟练,我所指的不是光光会搭建,而是要很熟悉里面的相当配置才行,因为公司最关键的绝对是WEB服务器,所以nginx和apache要熟悉,特别是nginx一定要很熟悉才行,至少有些公司还会用tomcat,这个也最好学一下。其实网络服务方面不用太担心,一般公司的环境都已经搭建好,就算有新服务器或让你整改,公司会有相应的文档让你参照来弄,不会让你乱来的,但至少相关的配置一定要学熟,而且肯定是编译安装多,那些模块要熟悉一下他的作用,特别是PHP那些模块。
这面2点只是基础,也是必要条件,不能说是工具,下以才是真正的要掌握的工具。

3、shell脚本和另一个脚本语言,shell是运维人员必须具备的,不懂这个连入职都不行,至少也要写出一些系统管理脚本,最简单也得写个监控CPU,内存比率的脚本吧,这是最最最基本了,别以为会写那些猜数字和计算什么数的,这些没什么作用,只作学习意义,写系统脚本才是最有意义,而另一个脚本语言是可选的,一般是3P,即python, perl和php,php就不需要考虑了,除非你要做开发,我个人建议学python会比较好,难实现自动化运维,perl是文本处理很强大,反正这两个学一个就行了。

4、sed和awk工具,必须要掌握,在掌握这两个工具同时,还要掌握正则表达式,这个就痛苦了,正则是最难学的表达式,但结合到sed和awk中会很强大,在处理文本内容和过滤WEB内容时十分有用,不过在学shell的同时一般会经常结合用到的,所以学第3点就会顺便学第4点。

5、文本处理命令,sort , tr , cut, paste, uniq, tee等,必学,也是结合第3点时一并学习的。

6、数据库,首选mysql,别问我为什么不学sqlserver和oracle,因为linux用得最多绝对是mysql,增删改查必学,特别要学熟查,其它方面可能不太需要,因为运维人员使用最多还是查,哪些优化和开发语句不会让你弄的。

7、防火墙,不学不行,防火墙也算是个难点,说难不难,说易不易,最重要弄懂规则,如果学过CCNA的朋友可能会比较好学,因为iptables也有NAT表,原理是一样的,而FILTER表用得最多,反正不学就肯定不合格。

8、监控工具,十分十分重要,我个人建议,最好学这3个,cacti,nagios,zibbix,企业用得最多应该是nagios和 zibbix,反正都学吧,但nagios会有点难,因为会涉及到用脚本写自动监控,那个地方很难。

9、集群和热备,这个很重要,肯定要懂的,但到了公司就不会让你去弄,因为新手基本不让你碰,集群工具有很多,最好学是LVS,这是必学,最好也学学nginx集群,反向代理,还有热备,这个就更多工具能实现了,像我公司是自己开发热备工具的,mysql热备也要学,就是主从复制,这个别告诉我容易,其实不容易的,要学懂整个流程一点也不容易,只照着做根本没意思。

10、数据备份,不学不行,工具有很多,但至少要把RAID的原理弄懂,特别是企业最常用的1+0或0+1,自己做实验也要弄出来,备份工具有很多,如tar, dump, rsync等,最好多了解一下。
算了,说到这10点已经够你受了,应该可以入门了,因为有些技术会比较难学,例如apache和nginx中还有些很重要的技术,如系统调优和服务优化,还有程序优化,这些在没接触工作前很难学习到的,所以先把这10点学了吧,估计要学熟至少3个月不止,就脚本那部分已经让你很吃力了,我建议是先学熟shell,等工作后再学另一门脚本语言,这样会比较好。

以上就是踏入linux运维工程师需要掌握的工具,其实还有很多工具要掌握的,但你在学习环境中是很难学到,最后我再提醒一下,这里所指的工具相当于技能,而不是像windows或ubuntu那些图形化工具,那些工具没用的,还有,学linux就别装图形界面,这样虚拟机就不用吃太多内存,而且绝对不建议在真机上装linux,根本达不到学习效果。

Linux运维工程师入门须掌握的10个技术点,首发于博客 - 伯乐在线

12 Jun 06:54

白板编程浅谈——Why, What, How

by promumu

这篇文章节选自我正在撰写的一本关于应届生面试求职的书籍,欢迎在评论或微博(@peng_gong)上留言反馈。

面试很困难,技术面试更加困难——只用 45 ~ 60 分钟是很难考察出面试者的水平的。所以 刘未鹏 在他的 怎样花两年时间去面试一个人 一文中鼓励面试者创建 GitHub 账号,阅读技术书籍,建立技术影响力,从而提供给面试官真实,明确,可度量的经历。

这种方法对面试者效果很好,但对面试官效果就很一般——面试官要面对大量的面试者,这些面试者之中可能只有很少人拥有技术博客,但这并不代表他们的技术能力不够强(也许他们对写作不感兴趣);另一方面,一些人拥有技术博客,但这也不能说明他们的水平就一定会很牛(也许他们在嘴遁呢)。

总之,技术博客和 GitHub 账号是加分项,但技术面试仍然必不可少。所以,问题又回来了,如何进行高效的技术面试?或者说,如何在 45 ~ 60 分钟内尽可能准确的考察出面试者的技术水平?

回答这个问题之前,让我们先看下技术面试中的常见问题都有什么:

 

技术面试中的常见问题

技术面试中的问题大致可以分为 5 类:

  • 编码:考察面试者的编码能力,一般要求面试者在 20 ~ 30 分钟之内编写一段需求明确的小程序(例:编写一个函数划分一个整形数组,把负数放在左边,零放在中间,正数放在右边);
  • 设计:考察面试者的设计/表达能力,一般要求面试者在 30 分钟左右内给出一个系统的大致设计(例:设计一个类似微博的系统)
  • 项目:考察面试者的设计/表达能力以及其简历的真实度(例:描述你做过的 xxx 系统中的难点,以及你是如何克服这些难点)
  • 脑筋急转弯:考察面试者的『反应/智力』(例:如果你变成蚂蚁大小然后被扔进一个搅拌机里,你将如何脱身?)
  • 查漏:考察面试者对某种技术的熟练度(例:Java 的基本类型有几种?)

这 5 类问题中,脑筋急转弯在外企中早已绝迹(因为它无法判定面试者的真实能力),查漏类问题因为实际价值不大(毕竟我们可以用 Google)在外企中出现率也越来越低,剩下的 3 类问题里,项目类和设计类问题要求面试官拥有同类项目经验,只有编码类问题不需要任何前提,所以,几乎所有的技术面试中都包含编码类问题。

然而,最令面试者头痛的也是这些编码类问题——因为几乎所有的当面(On-site)技术面试均要求面试者在白板上写出代码,而不是在面试者熟悉的 IDE 或是编辑器中写出。在我的面试经历里,不止一个被面试者向我抱怨:『如果能在计算机上编程,我早就把它搞定了!』就连我自己在面试初期也曾怀疑白板代码的有效性:『为什么不让面试者在计算机上写代码呢?』

然而在经历了若干轮被面试与面试之后,我惊奇的发现白板编程竟然是一种相当有效的技术考察方式。这也是我写这篇文章的原因——我希望通过这篇文章来阐述为什么要进行白板编程(WHY),什么是合适的白板编程题目(WHAT),以及如何进行白板编程(HOW),从而既帮助面试者更好的准备面试,也帮助面试官更好的进行面试。

 

为什么要进行白板编程

很多面试者希望能够在 IDE 中(而不是白板上)编写代码,因为:

  • 主流 IDE 均带有智能提示,从而大大提升了编码速度
  • IDE 可以保证程序能够编译通过
  • 可以通过 IDE 运行/调试代码,找到程序的 Bug

我承认第 1 点,白板编程要比 IDE 编程慢很多,但这并不能做为否认白板编程的理由——因为白板编程往往是 API 无关(因此并不需要你去背诵 API)的一小段(一般不超过 30 行)代码,而且面试官也会允许面试者进行适当的缩写(比如把Iterable类型缩写为Iter),因此它并不能成为否认白板编程的理由。

至于第 2 点和第 3 点,它们更不能成为否认白板编程的借口——如果你使用 IDE 只是为了在其帮助下写出能过编译的代码,或是为了调试改 Bug,那么我不认为你是一名合格的程序员——我认为程序员可以被分为两种:

  • 先确认前条件/不变式/终止条件/边界条件,然后写出正确的代码
  • 先编写代码,然后通过各种用例/测试/调试对程序进行调整,最后得到似乎正确的代码

我个人保守估计前者开发效率至少是后者的 10 倍,因为前者不需要浪费大量时间在 编码-调试-编码 这个极其耗时的循环上。通过白板编程,面试官可以有效的判定出面试者属于前者还是后者,从而招进合适的人才,并把老油条或是嘴遁者排除在外。

除了判定面试者的开发效率,白板编程还有助于展示面试者的编程思路,并便于面试者和面试官进行交流:

白板编程的目标并不是要求面试者一下子写出完美无缺的代码,而是:

  • 让面试者在解题的过程中将他/他的思维过程和编码习惯展现在面试官面前,以便面试官判定面试者是否具备清晰的逻辑思维和良好的编程素养
  • 如果面试者陷入困境或是陷阱,面试官也可以为其提供适当的辅助,以免面试陷入无人发言的尴尬境地

 

什么是合适的白板编程题目

正如前文所述,白板编程是一种很有效的技术面试方式,但这是建立在有效的编程题目的基础之上:如果编程题目过难,那么面试很可能会陷入『大眼瞪小眼』的境地;如果编程题目过于简单(或者面试者背过题目),那么面试者无需思考就可以给出正确答案。这两种情况都无法达到考察面试者思维过程的目的,从而使得面试官无法正确评估面试者的能力。

既然编程题目很重要,那么问题来了,什么才是合适(合理)的编程题目呢?

在回答这个问题之前,让我们先看看什么编程题目不合适:

什么不该问

1.被问滥的编程问题

我在求职时发现,技术面试的编程题目往往千篇一律——拿我自己来说,反转单链表被问了 5 次,数字转字符串被问了 4 次,随机化数组被问了 3 次,最可笑的是在面试某外企时三个面试官都问我如何反转单链表,以至于我得主动要求更换题目以免误会。

无独有偶,我在求职时同时发现很多面试者都随身带一个本子或是打印好的材料,上面写满了常见的面试题目,一些面试者甚至会祈祷能够被问到上面的题目。

就这个问题,我和我的同学以及后来的同事讨论过,答案是很多面试官在面试前并不会提前准备面试题,而是从网络上(例如 July 的算法博客)或 编程之美 之类的面试题集上随机挑一道题目询问。如果面试者做出来(或背出来)题目那么通过,如果面试者做不出来就挂掉。

这种面试方式的问题非常明显:如果面试者准备充分,那么这些题目根本没有区分度——面试者很可能会把答案直接背下来;如果面试者未做准备,他/她很可能被一些需要 aha! moment 的题目困住。总之,如果面试题不能评估面试者水平,那么问它还有什么意义呢?

下面是一些问滥的编程问题:

  • 编程之美 书里的所有题目;
  • July 的算法博客 中的绝大多数题目(包括 面试 100 题 中的所有题目);
  • leecode 里的大部分题目;

2.涉及到库函数或 API 调用

白板编程的目标在于考察面试者的编程基本功,而不是考察面试者使用某种语言/类库的熟练度。所以白板编程题目应尽可能库函数无关——例如:编写一个 XML 读取程序就是不合格的题目,因为面试者没有必要把 XML 库中的函数名背下来(不然要 Intellisense 干甚);而原地消除字符串的重复空白(例:”ab c d e” => “abcde”)则是一道合格的题目,因为即便不使用库函数,合格的面试者也能够在 20 分钟内完成这道题目。

3.过于直接(或简单)的算法问题

这类问题类似 被问滥的编程问题,它们的特点在于过于直接,以至于面试者不需要思考就可以给出答案,从而使得面试官无法考察面试者的思维过程。快速排序,深度优先搜索,以及二分搜索都属于这类题目。

需要注意的是,尽管过于直接的算法题目不适合面试,但是我们可以将其进行一点改动,从而使其变成合理的题目,例如稳定划分和二分搜索计数(给出有序数组中某个元素出现的次数)就不错,尽管它们实际是快速排序和二分搜索的变种。

4.过于复杂的题目

同 过于直接的算法问题< 相反,过于复杂的题目 属于另一个极端:这些题目往往要求面试者拥有极强的算法背景,尽管算法问题是否过于复杂因人而异(在一些 ACM 编程竞赛选手的眼里可能就没有复杂的题目 –_-),但我个人认为如果一道题满足了下面任何一点,那么它就太复杂,不适合面试(不过如果面试者是 ACM 编程竞赛选手,那么可以无视此规则):

  • 需要 aha! moment(参考 脑筋急转弯)
  • 需要使用某些『非主流』数据结构/算法才能求解
  • 耗时过长(例如实现红黑树的插入/删除)

5.脑筋急转弯

什么是脑筋急转弯?

  • 不考察编程能力
  • 依赖于 aha! moment
  • All or nothin:或者做不出来,或者是最终答案

在一些书(例如 谁是谷歌想要的人才?:破解世界最顶尖公司的面试密码)和电影的渲染下,Google 和微软这些外企的面试被搞的无比神秘,以至于很多人以为外企真的会问诸如『井盖为什么是圆的』或是『货车能装多少高尔夫球』这样的奇诡问题。而实际上,这些题目由于无法考察面试者的技术能力而早已在外企中绝迹。反倒是一些国内公司开始使用脑筋急转弯 作为面试题目 –_–#

 

应该问什么问题

所以,技术面试题目不应该太难,也不应太简单,不能是脑筋急转弯,也不能直接来自网络。

前三点并不难满足:我们可以去 算法导论,编程珠玑,以及 计算机程序设计艺术 这些经典算法书籍中的课后题/练习题挑选合适的题目,也可以自己创造题目。然而,由于 careercup 这类网站的存在,没有什么题目可以做到绝对原创——毕竟没有人能阻止面试者把题目发到网上,所以任何编程题目都逃脱不了被公开的命运。

不过,尽管面试者会把编程题目发到网上,甚至会有一些『好心人』给出答案,但这并不代表面试官不能继续使用这道题:因为尽管题目被公开,但题目的考察点和延伸问题依然只有面试官才知道。这有点像 公钥加密,公钥(面试题)是公开的,但私钥(解法,考察点,以及延伸问题)只有面试官才知道。这样即便面试者知道面试题,也不会妨碍面试官考察面试者的技术能力。

接下来,让我们看看什么问题适合白板编程。

1.不止一种解法

良好的编程问题都会有不止一种解法。这样面试者可以在短时间内给出一个不那么聪明但可实现的『粗糙』算法,然后通过思考(或面试官提示)逐步得到更加优化的解法,面试官可以通过这个过程观察到面试者的思维方式,从而对面试者进行更客观的评估。

以 数组最大子序列和 为例,它有一个很显然的 O(n3) 解法,将 O(n3) 解法稍加改动可以得到 O(n2) 解法,利用分治思想,可以得到 O(n*logn) 解法,除此之外它还有一个 o(n) 解法。(编程珠玑 和 数据结构与算法分析 C语言描述 对这道题均有非常精彩的描述,有兴趣的朋友可以自行阅读)

2.考察点明确

良好的编程问题应拥有大量考察点,面试官应对这些考察点烂熟于心,从而给出更加客观量化的面试结果。这里可以参考我之前在 从武侠小说到程序员面试 提到的 to_upper。

3.延伸问题

良好的编程问题应拥有延伸问题。延伸问题既可以应对面试者背题的情况,也可以渐进的(Incremental)考察面试者的编程能力,同时还保证了面试的延续性(Continuity)。

以 遍历二叉树 为例:面试官可以从非递归中序遍历二叉树开始提问,面试者有可能会很快的写(或是背)出一个使用栈的解法。这时面试官可以通过延伸问题来判别面试者是否在背题:使用常量空间中序遍历带有父节点指针的二叉树,或是找到二叉搜索树中第 n 小的元素。下面是中序遍历二叉树的一些延伸问题:

|--中序遍历二叉树
|
|--非递归中序遍历二叉树
|
|--常量空间,非递归遍历带父节点的二叉树
| |
| |--在带父节点的二叉搜索树寻找第 N 小的元素
| |
| |--可否进一步优化时间复杂度?
|
|--常量空间,非递归遍历不带父节点的二叉树

上面的问题不但可以被正向使用(逐步加强难度),也可以被逆向使用(逐步降低难度):同样从非递归中序二叉树遍历开始提问,如果面试者无法完成这个问题,那么面试官可以降低难度,要求面试者编写一个递归版本的中序遍历二叉树。

 

如何进行白板编程

面试官应该做什么

面试前

面试之前,面试官应至少得到以下信息:

  • 面试者的简历
  • 面试者的应聘职位
  • 面试者之前被问过哪些面试题

接下来,面试官应根据面试者的简历/职位确认对面试者的期望值,然后准备好编程题目(而不是面试时即兴选择题目)。面试官应至少准备 4 道题目(2 道简单题,2 道难题),以应对各种情况。

面试中

面试时,面试官应清楚的陈述题目,并通过若干组用例数据确认面试者真正的理解题目(以免面试者花很长时间去做不相关的题目,我在之前的面试就办过这种挫事 –_–#)

在面试者解题时,面试官应全程保持安静(或倾听的状态),如果面试者犯下特别严重的错误或是陷入苦思冥想,面试官应给出适当的提示,以帮助面试者走出困境完成题目,如果面试者还是不能完成题目,那么面试官应换一道略简单的题目,要知道面试的目的是发现面试者的长处,而非为难面试者。(一些国内企业似乎正好相反)

面试后

面试之后,面试官应拍照(或誊写)面试者写下的代码,然后把提问的问题发给 HR 和接下来的面试者(以确保问题不会重复)。接下来,面试官应根据面试者的代码以及其面试表现,尽快写出面试反馈(Interview Feedback)发给 HR,以便接下来的招聘流程。

 

面试者应该做什么

面试前

面试之前,面试者应至少做过以下准备:

  • 拥有扎实的数据结构/算法基础
  • 知道如何利用 前条件/不变式/后条件 这些工具编写正确的程序
  • 能够在白板(或纸上)实现基本的数据结构和算法(如果 1 和 2 做到这一步是水到渠成)
  • 在 leetcode 或 careercup 上面进行过练习,了解常见的技术面试题目(我个人不鼓励刷题,但在面试前建立起对面试题的『感觉』非常重要)

面试中

确定需求

面试者在白板编程时最重要的任务是理解题目,确认需求——确定输入/输出,确定数据范围,确定时间/空间要求,确定其它限制。以最常见的排序为例:

  • 输入:来自数组?链表?或是不同的机器?
  • 输出:是否有重复?是否要求稳定?
  • 数据范围:排序多少个元素?100 个? 100 万个? 1 亿个?这些元素是否在某个范围内?
  • 时间要求:1 分钟?1 刻钟?一小时?
  • 空间要求:是否常量空间?是否可以分配新的空间?如果可以,能分配多少空间?是否在内存中排序?
  • 其它限制:是否需要尽可能少的赋值?是否需要尽可能少的比较?

有时面试官不会把题目说的特别清楚,这时就需要面试者自己去确认这些需求,不要认为这是在浪费时间,不同的需求会导致截然不同的解法,此外确认需求会留给面试官良好的印象。

白板编程

理解题目确认需求之后,面试者就可以开始在白板上编写代码,下面是一些我自己的白板编程经验:

  • 先写出轮廓(大纲)

白板编程没法复制粘贴,所以后期调整代码结构非常困难。因此我们最好在开头写出程序的大致结构,从而保证之后不会有大改;

  • 确定前条件/不变式/后条件

我们可以通过注释的形式给出代码的前条件/不变式/后条件,以划分为例:

int* partition(int *begin, int *end, int pivot) {
     int *par = begin;
     for ( ; begin < end; begin++) {
        if (*begin < pivot) {
        swap(begin, par++)
        }
    }
    return par;
}

就不如

int* partition(int *begin, int *end, int pivot) {
     // [begin, end) should be a valid range
     int *par = begin;
     // Invariant: All [0, par) < pivot && All [par, begin) >= pivot
     for ( ; begin < end; begin++) {
          if (*begin < pivot) {
          swap(begin, par++)
          }
     }
     // Now All [0, par) < pivot && All [par, end) >= pivot
     return par;
}

使用实例数据验证自己的程序
尽管不变式足以验证程序的正确性,但适当的使用实例数据会大大增强代码的可信性,以上面的划分程序为例:

Given range [2, 3, 4, 5, 1] and pivot 3

[ 2, 3, 4, 5, 1 ]
^ ^
p,b e

[ 2, 3, 4, 5, 1 ]
^ ^
p,b e

[ 2, 3, 4, 5, 1 ]
^ ^ ^
p b e

[ 2, 3, 4, 5, 1 ]
^ ^ ^
p b e

[ 2, 1, 4, 5, 3 ]
^ ^ ^
p b e

[ 2, 1, 4, 5, 3 ]
^ ^
p b,e

Now we have all [0, p) < 3 and all [p, e) >= 3
  • 使用缩写

白板编程并不需要面试者在白板上写出能够一次通过编译的代码。为了节省时间,面试者可以在和面试官沟通的基础上使用缩写。例如使用 Iter 替代 Iterable,使用 BQ 替代 BlockingQueue。(此法尤其适合于 Java –_–#)

  • 至少留一行半行宽

出于紧张或疏忽,一般面试者在白板编程时会犯下各种小错误,例如忘了某个判断条件或是漏了某条语句,空余的行宽可以帮助面试者快速修改代码,使得白板上的代码不至于一团糟。

这就延伸出了另一个问题,如果使用大行宽,那么白板写不下怎么办?一些面试者聪明的解决了这个问题:他们在面试时会自带一根细笔迹的水笔,专门用于白板编程。

不会做怎么办

相信大多数面试者都碰到过面试题不会做的情况,这里说说我自己的对策:

  1. 至少先给出一个暴力(Brute force)解法
  2. 寻找合适的数据结构(例如栈/队列/树/堆/图)和算法(例如分治/回溯/动态规划/贪婪)
  3. 从小数据集开始尝试
  4. 如果还是没有头绪,重新考虑题目的前条件,思考是否漏掉了条件(或是隐含的条件)
  5. 如果 3 分钟过后还是没有任何思路,请求面试官提示,不要觉得不好意思——经过提示给出答案远强于没有答案

面试后

个人不建议面试者在面试之后把题目发到网上,很多公司在面试前都会和面试者打招呼,有的会签订 NDA(Non Disclosure Agreement)条款以确保面试者不会泄露面试题目。尽管他们很少真的去查,但如果被查到那绝对是得不偿失。

我自己在面试之后会把面试中的编程题目动手写一遍(除非题目过于简单不值得),这样既能够验证自己写的代码,也可以保证自己不会在同一个地方摔倒两次。

 

参考

书籍

  • Elements of Programming Interviews: The Insiders’ Guide
  • 编程原本
  • 程序员面试金典(第5版)

文章

以上。

 

白板编程浅谈——Why, What, How,首发于博客 - 伯乐在线

12 Jun 06:43

谈谈 Hash Table

by importnewzz

一.数据结构

在我们编程的世界里数据的基本组织可以说有三种形式。

  • 结构体(或对象)
  • 数组
  • 链表

其他任何的数据组织形式都可以看作是这三种数据组织形式的组合变体。

结构体(或对象)可以是基本数据类型或者其他结构体(或对象)的组合。结构体或对象一般用来描述一个复杂数据实体。

数组一般是一组同类型的变量的集合,在内存中表现为一片连续的空间,因为空间是连续的,且每一个数据单元占的内存空间的大小是相等的,所以可以根据地址的偏移对数据元素实现快速访问,但是当需要插入或者删除一个元素的时候,则需要对目标元素的之后的所有元素进行移动了。 链表的单个节点一般为结构体或者对象,因为链表的单个节点除了需要保存数据之外还需要维护它的相邻节点的关系,如果想获得链表中的某个节点的值,需要从链表的头结点开始遍历,直到找到需要的东西,而插入或者删除某个节点的话,需要找到相应的节点,修改其以及其相邻节点的相关指针的引用即可。

像其他的数据结构,比如 队列,栈,树,都可以通过数组或者链表来组织,并实现相应的操作功能。

二.Hash Table

这个世界上没有十全十美的东西,所以我们要学会取舍。任何技术的实现都没有最好的只要最合适的,也就说实现的最佳方案是和应用场景息息相关的。

很多时候,我们想对数据进行快速的存取(比如缓存的实现),并用一个key来标记自己存取的数据。我们可以把它叫做key-value的结构。
说到“快速”我们很快想到数组,因为数组可以在O(1)的时间复杂内完成指定位置元素的读写操作。

所以在理想状态,如果一个数组足够长,且存在一个函数可以将每一个key映射到唯一的一个数组下标,那么我们就可以很完美的解决问题。但往往资源都是有限的,我们没有那么大的空间,也不能设计一个无比负责的映射算法保证每一个key对应到一个唯一的数组下标。所以我们会选择一些折中的方案。

hash table便是为解决这类问题而存在的。

1.哈希函数

Hash或者你可以翻译成散列或者杂凑,hash操作其本质上就是将一个数据映射成另一个数据,通常情况下原数据的长度比hash后的数据容量大。 这种映射的关系我们叫做哈希函数。

一般情况下 哈希函数的输入可能的总数要远远多于哈希值所能表示的总数,所以就有可能两个不同的输入对应同一个哈希值,通常把具有不同关键码而具有相同哈希值的记录称作“同义词”。 在信息安全领域中也经常使用到哈希函数,不过需要使用的是单向哈希函数,就是无法通过哈希的结果反推出输入,所以经常应用于密码的加密,传输内容的完整性检查,在安全领域常用的哈希算法有 MD5,SHA1等。 在哈希表的应用中,哈希函数常用余数法进行,也就是通过求模的方式算出哈希值。

2.哈希表

哈希表是一种数据结构,实现key-value的快速存取。之前说过数组可以实现快速存取,所以哈希表肯定会使用到数组。在这里,我们把每一个数组的单元叫做一个bucket(桶)。

构造哈希函数 这里哈希函数的作用就是将key映射到一个存储地址。所以构造一个哈希表我们得先构造哈希函数。 如果一个key哈希后对应地址中已经存放了值了,这种情况我们叫做哈希冲突(Hash collisions)。 如果存在一个哈希函数,使得每一个输入都能对应到唯一的一个存储单元中(没有冲突),那么这样的哈希函数我们可以叫它完美哈希函数(Perfect Hash Function,简称PHF)。 但为了哈希函数简单,运行速度快,往往不会使用完美哈希函数。所以冲突肯定会存在的,为了减少冲突,我们希望哈希函数的结果均匀的分布在地址单元的空间中。这样可以有效的减少冲突。

装填因子Load factor a=哈希表的实际元素数目(n)/ 哈希表的容量(m) a越大,哈希表冲突的概率越大,但是a越接近0,那么哈希表的空间就越浪费。 一般情况下建议Load factor的值为0-0.7,Java实现的HashMap默认的Load factor的值为0.75,当装载因子大于这个值的时候,HashMap会对数组进行扩张至原来两倍大。

冲突解决 既然冲突不可避免,那么我们就必须对冲突进行解决(总不能把之前的内容覆盖掉把), 解决冲突的方式主要分两类 开放定址法(Open addressing)这种方法就是在计算一个key的哈希的时候,发现目标地址已经有值了,即发生冲突了,这个时候通过相应的函数在此地址后面的地址去找,直到没有冲突为止。这个方法常用的有线性探测,二次探测,再哈希。 这种解决方法有个不好的地方就是,当发生冲突之后,会在之后的地址空间中找一个放进去,这样就有可能后来出现一个key哈希出来的结果也正好是它放进去的这个地址空间,这样就会出现非同义词的两个key发生冲突。

链接法(Separate chaining)链接法是通过数组和链表组合而成的。当发生冲突的时候只要将其加到对应的链表中即可。

与开放定址法相比,链接法有如下几个优点:

①链接法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于链接法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而链接法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用链接法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

当然链接法也有其缺点,拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

可能感兴趣的文章

12 Jun 00:59

Java内存模型与volatile关键字

by importnewzz

Java内存模型(Java Memory Model)

Java内存模型(JMM),不同于Java运行时数据区,JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取数据这样的底层细节。JMM规定了所有的变量都存储在主内存中,但每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,工作内存是线程之间独立的,线程之间变量值的传递均需要通过主内存来完成。

volatile关键字

平时在阅读jdk源码的时候,经常看到源码中有写变量被volatile关键字修饰,但是却不是十分清除这个关键字到底有什么用处,现在终于弄清楚了,那么我就来讲讲这个volatile到底有什么用吧。

当一个变量被定义为volatile之后,就可以保证此变量对所有线程的可见性,即当一个线程修改了此变量的值的时候,变量新的值对于其他线程来说是可以立即得知的。可以理解成:对volatile变量所有的写操作都能立刻被其他线程得知。但是这并不代表基于volatile变量的运算在并发下是安全的,因为volatile只能保证内存可见性,却没有保证对变量操作的原子性。比如下面的代码:

/**
 * 发起20个线程,每个线程对race变量进行10000次自增操作,如果代码能够正确并发,
 * 则最终race的结果应为200000,但实际的运行结果却小于200000。
 * 
 * @author Colin Wang
 *
 */
public class VolatileTest {
	public static volatile int race = 0;

	public static void increase() {
		race++;
	}

	private static final int THREADS_COUNT = 20;

	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];

		for (int i = 0; i < THREADS_COUNT; i++) {
			threads[i] = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
						increase();
					}
				}
			});
			threads[i].start();
		}
		
		while (Thread.activeCount() > 1)
			Thread.yield();

		System.out.println(race);
	}
}

这便是因为race++操作不是一个原子操作,导致一些线程对变量race的修改丢失。若要使用volatale变量,一般要符合以下两种场景:

  1. 变量的运算结果并不依赖于变量的当前值,或能够保证只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

使用volatile变量还可以禁止JIT编译器进行指令重排序优化,这里使用单例模式来举个例子:

/**
 * 单例模式例程一
 * 
 * @author Colin Wang
 *
 */
public class Singleton_1 {

	private static Singleton_1 instance = null;

	private Singleton_1() {
	}

	public static Singleton_1 getInstacne() {
		/*
		 * 这种实现进行了两次instance==null的判断,这便是单例模式的双检锁。
		 * 第一次检查是说如果对象实例已经被创建了,则直接返回,不需要再进入同步代码。
		 * 否则就开始同步线程,进入临界区后,进行的第二次检查是说:
		 * 如果被同步的线程有一个创建了对象实例, 其它的线程就不必再创建实例了。
		 */
		if (instance == null) {
			synchronized (Singleton_1.class) {
				if (instance == null) {
					/*
					 * 仍然存在的问题:下面这句代码并不是一个原子操作,JVM在执行这行代码时,会分解成如下的操作:
					 * 1.给instance分配内存,在栈中分配并初始化为null
					 * 2.调用Singleton_1的构造函数,生成对象实例,在堆中分配 
					 * 3.把instance指向在堆中分配的对象
					 * 由于指令重排序优化,执行顺序可能会变成1,3,2,
					 * 那么当一个线程执行完1,3之后,被另一个线程抢占,
					 * 这时instance已经不是null了,就会直接返回。
					 * 然而2还没有执行过,也就是说这个对象实例还没有初始化过。
					 */
					instance = new Singleton_1();
				}
			}
		}
		return instance;
	}
}
/**
 * 单例模式例程二
 * 
 * @author Colin Wang
 *
 */
public class Singleton_2 {

	/*
	 * 为了避免JIT编译器对代码的指令重排序优化,可以使用volatile关键字,
	 * 通过这个关键字还可以使该变量不会在多个线程中存在副本,
	 * 变量可以看作是直接从主内存中读取,相当于实现了一个轻量级的锁。
	 */
	private volatile static Singleton_2 instance = null;

	private Singleton_2() {
	}

	public static Singleton_2 getInstacne() {
		if (instance == null) {
			synchronized (Singleton_2.class) {
				if (instance == null) {
					instance = new Singleton_2();
				}
			}
		}
		return instance;
	}
}

变量在有了volatile修饰之后,对变量的修改会有一个内存屏障的保护,使得后面的指令不能被重排序到内存屏障之前的位置。volalite变量的读性能与普通变量类似,但是写性能要低一些,因为它需要插入内存屏障指令来保证处理器不会发生乱序执行。即便如此,大多数场景下volatile的总开销仍然要比锁低,所以volatile的语义能满足需求时候,选择volatile要优于使用锁。

相关文章

12 Jun 00:52

Java内存模型

by BlankKelly

原文地址  作者:Jakob Jenkov 译者:张坤

Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型。

如果你想设计表现良好的并发程序,理解Java内存模型是非常重要的。Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

原始的Java内存模型存在一些不足,因此Java内存模型在Java1.5时被重新修订。这个版本的Java内存模型在Java8中人在使用。

Java内存模型内部原理

Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。

Java Memory Model

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。

enter image description here

一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。

一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。

一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。

一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。

静态成员变量跟随着类定义一起也存放在堆上。

存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。

下图演示了上面提到的点:

enter image description here

两个线程拥有一些列的本地变量。其中一个本地变量(Local Variable 2)执行堆上的一个共享对象(Object 3)。这两个线程分别拥有同一个对象的不同引用。这些引用都是本地变量,因此存放在各自线程的线程栈上。这两个不同的引用指向堆上同一个对象。

注意,这个共享对象(Object 3)持有Object2和Object4一个引用作为其成员变量(如图中Object3指向Object2和Object4的箭头)。通过在Object3中这些成员变量引用,这两个线程就可以访问Object2和Object4。

这张图也展示了指向堆上两个不同对象的一个本地变量。在这种情况下,指向两个不同对象的引用不是同一个对象。理论上,两个线程都可以访问Object1和Object5,如果两个线程都拥有两个对象的引用。但是在上图中,每一个线程仅有一个引用指向两个对象其中之一。

因此,什么类型的Java代码会导致上面的内存图呢?如下所示:

public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}


public class MySharedObject {

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}

如果两个线程同时执行run()方法,就会出现上图所示的情景。run()方法调用methodOne()方法,methodOne()调用methodTwo()方法。

methodOne()声明了一个原始类型的本地变量和一个引用类型的本地变量。

每个线程执行methodOne()都会在它们对应的线程栈上创建localVariable1localVariable2的私有拷贝。localVariable1变量彼此完全独立,仅“生活”在每个线程的线程栈上。一个线程看不到另一个线程对它的localVariable1私有拷贝做出的修改。

每个线程执行methodOne()时也将会创建它们各自的localVariable2拷贝。然而,两个localVariable2的不同拷贝都指向堆上的同一个对象。代码中通过一个静态变量设置localVariable2指向一个对象引用。仅存在一个静态变量的一份拷贝,这份拷贝存放在堆上。因此,localVariable2的两份拷贝都指向由MySharedObject指向的静态变量的同一个实例。MySharedObject实例也存放在堆上。它对应于上图中的Object3。

注意,MySharedObject类也包含两个成员变量。这些成员变量随着这个对象存放在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的Object2和Object4.

注意,methodTwo()创建一个名为localVariable的本地变量。这个成员变量是一个指向一个Integer对象的对象引用。这个方法设置localVariable1引用指向一个新的Integer实例。在执行methodTwo方法时,localVariable1引用将会在每个线程中存放一份拷贝。这两个Integer对象实例化将会被存储堆上,但是每次执行这个方法时,这个方法都会创建一个新的Integer对象,两个线程执行这个方法将会创建两个不同的Integer实例。methodTwo方法创建的Integer对象对应于上图中的Object1和Object5。

还有一点,MySharedObject类中的两个long类型的成员变量是原始类型的。因为,这些变量是成员变量,所以它们任然随着该对象存放在堆上,仅有本地变量存放在线程栈上。

硬件内存架构

现代硬件内存模型与Java内存模型有一些不同。理解内存模型架构以及Java内存模型如何与它协同工作也是非常重要的。这部分描述了通用的硬件内存架构,下面的部分将会描述Java内存是如何与它“联手”工作的。

下面是现代计算机硬件架构的简单图示:

enter image description here

一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中可以有一个缓存层就可以了。

一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。

通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

Java内存模型和硬件内存架构之间的桥接

上面已经提到,Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:

enter image description here

当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。主要包括如下两个方面:

-线程对共享变量修改的可见性
-当读,写和检查共享变量时出现race conditions

下面我们专门来解释以下这两个问题。

共享对象可见性

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不接见的。

想象一下,共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中。然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。

enter image description here

解决这个问题你可以使用Java中的volatile关键字。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。

Race Conditions

如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions

想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增在了两个,每个CPU缓存中一次。

如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。

然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次。

下图演示了上面描述的情况:

enter image description here

解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java内存模型

11 Jun 00:25

Rust/Go/Node.js/Io.js/Groovy/Scala/Lua 语言入门 Ⅰ

by landon
     摘要: Go,Rust,Node.js,Groovy,Scala,Lua几种语言入门初探,入门篇,比较各个语言的不同!  阅读全文

landon 2015-06-10 21:36 发表评论
10 Jun 02:48

通过 GDB 学习 C 语言

by myillusion3852

对于那些具有高级编程语言诸如: Ruby、Scheme、Haskell 等背景的人来说,学习 C 语言是具有挑战性的。除了纠结于 C  语言中像手动内存管理和指针等底层特性外,你必须在没有 REPL ( Read-Eval-Print Loop ) 的条件下完成工作。一旦你已经习惯于在 REPL 环境下进行探索性的编程,必须进行“编写-编译-运行”这样循环实在有点令人生厌。

最近我发现其实可以用 GDB 来作为 C 语言的伪 REPL。我一直尝试使用 GDB 作为学习 C 语言的工具,而不仅仅是用来调试 C 程序,事实上这非常有趣。

这篇文章我的目的就是向你展示 GDB 是一个非常好的学习 C 语言工具。下面我将会向你介绍一些我最喜欢的 GDB 命令,然后我会向你阐述怎样使用 GDB 来理解 C 语言中一个出了名的复杂问题:数组和指针的区别。

GDB 简介

从创建一个简单的 C 程序开始,minimal.c:

int main()
{ 
   int i = 1337;
   return 0; 
}

注意这个程序并没有做任何事情,也没有一条输出指令。拥抱使用 GDB 学习 C 语言的美丽新世界吧!

使用 -g 参数进行编译,这样会生成一些有助于 debug,gdb 可以利用的信息,编译后用 GDB 运行起来:

$ gcc -g minimal.c -o minimal 
$ gdb minimal

你现在应该能看到明显的 GDB 提示行。我之前告诉你这是一个 REPL,下面我们就来试试:

(gdb) print 1 + 2 
$1 = 3

多么神奇! print 是 GDB 的内置命令,他能够打印出一个 C 语言命令的返回值。如果你不确定一个 GDB 命令是做什么,尝试在 GDB 提示下运行命令 help。

然后是一个更有趣的例子:

(gbd) print (int) 2147483648
$2 = -2147483648

这里我先忽略为什么 2147483648 == -2147483648;我想要说明的是即使是算术运算在 C 语言中也是有很多坑的,GDB 能够理解运行 C 语言中的算术运算。

现在让我们在主函数中设置一个断点然后运行程序:

(gdb) break main
(gdb) run

现在程序在第 3 行处暂停,正好在 i 进行初始化之前。有趣的是,尽管 i 还没有被初始化,我们依然能够使用 print 命令看到它的值。

(gdb) print i
$3 = 32767

在 C 语言中,一个未被初始化的局部变量的值是没有定义的,所以你用 GDB 打印出的值可能与这里的不一样。

我们可以用 next 命令来执行当前断点这一行:

(gdb) next
(gdb) print i
$4 = 1337

使用 x 命令检查内存

在 C 语言中变量用来标示一块连续的内存区间。一个变量的内存区间由两个数字决定:

  1. 这块内存第一个字节数的数值地址
  2. 内存的大小,单位是字节。变量所占内容的大小取决于变量的类型。

C 语言中一个独特的特性是你能够直接访问变量所占的内存。操作符 & 可以计算一个变量的地址,操作符 sizeof 计算变量所占内存的大小。

你可以在 GDB 中测试以上两个概念:

(gdb) print &i
$5 = (int *) 0x7fff5fbff584
(gdb) print sizeof(i)
$6 = 4

字面上看,i 所占内存起始于地址 0x7fff5fbff5b4,占内存 4 个字节。

我前面提到的变量在内存中的大小取决于它的类型,所以操作符 sizeof 能够直接作用于类型:

(gdb) print sizeof(int)
$7 = 4
(gdb) print sizeof(double)
$8 = 8

以上显示意味着,至少在我的计算机上 int 变量占 4 个字节空间,double 变量占 8 个字节。

GDB 带来了一个功能强大的工具,能够直接检测内存:x 命令。x 命令从一个特定的地址开始检测内存。结合一些结构化的命令和这些已给的命令能精确控制你想检测多少字节,你想怎样打印它们。当你有疑问时,尝试在 GDB 提示下运行 help x。

& 操作符计算变量的地址,这意味着我们能将 &i 返回给 x,从而看到 i 值背后原始的字节。

(gdb) x/4xb &i
0x7fff5fbff584: 0x39    0x05    0x00    0x00

标识参数表示我想要检查 4 个值,格式是十六进制,一次显示一个字节。我选择检查 4 个字节,是因为 i 在内存中的大小是 4 字节;逐字节打印出 i 在内存中的表示。

在 Intel 机器上有一个坑应当记得,逐字节检测时字节数是以“小端”顺序保存:不像人类一般使用的标记方法,一个数字的低位在内存中排在前面(个位数在十位数之前)。

为了让这个问题更加明显,我们可以为 i 赋一个特别的值,然后重新检测所占内存。

(gdb) set var i = 0x12345678
(gdb) x/4xb &i
0x7fff5fbff584: 0x78 0x56 0x34 0x12

使用 ptype 检查类型

ptype 命令可能是我最喜爱的命令。它告诉你一个 C 语言表达式的类型。

(gdb) ptype i
type = int
(gdb) ptype &i
type = int *
(gdb) ptype main
type = int (void)

C 语言中的类型可以变得很复杂,但是好在 ptype 允许你交互式地查看他们。

指针和数组

数组在C语言中是非常难以捉摸的概念。这节的计划是写出一个简单的程序,然后在 GDB 中运行,直至它的意义变得清晰易懂。

编写如下的程序,array.c:

int main()
{
    int a[] = {1,2,3};
    return 0;
}

使用 -g 作为命令行参数进行编译,在 GDB 中运行,然后输入 next,执行初始化那一行

$ gcc -g arrays.c -o arrays
$ gdb arrays
(gdb) break main
(gdb) run
(gdb) next

在这里,你应该能够打印出 a 的内容并检查它的类型:

(gdb) print a
$1 = {1, 2, 3}
(gdb) ptype a
type = int [3]

现在我们的程序已经在 GDB 中运行起来了,我们应该做的第一件事是使用 x 看看 a 在内存中是什么样子。

(gdb) x/12xb &a
0x7fff5fbff56c: 0x01  0x00  0x00  0x00  0x02  0x00  0x00  0x00
0x7fff5fbff574: 0x03  0x00  0x00  0x00

以上意思是 a 所占内存开始于地址 0x7fff5fbff5dc。起始的四个字节存储 a[0], 随后的四个字节存储 a[1], 最后的四个字节存储 a[2]。事实上你可以通过 sizeof 得到,a 在内存中的大小是 12 字节。

(gdb) print sizeof(a)
$2 = 12

现在,数组好像确实有个数组的样子。他们有自己的数组类型,在连续的内存空间中存储自己的成员。然而在某些情况下,数组表现得更像指针。例如,我们能在 a 上进行指针运算。

= preserve do
  :escaped
    (gdb) print a + 1
    $3 = (int *) 0x7fff5fbff570

字面上看,a+1 是一个指向 int 的指针,占据地址 0x7fff5fbff570。这时,你应该反过来将指针传递给 x 命令,让我们看看会发生什么:

= preserve do
  :escaped
    (gdb) x/4xb a + 1
    0x7fff5fbff570: 0x02  0x00  0x00  0x00

注意 0x7fff5fbff570 比 0x7fff5fbff56c 大 4,后者是 a 在内存地址中的第一个字节。考虑到 int 值占 4 字节,这意味着 a+1 指向 a[1].

事实上,在 C 语言中数组索引是指针运算的语法糖:a[i] 等于 *(a+i)。你可以在 GDB 中尝试一下。

= preserve do
  :escaped
    (gdb) print a[0]
    $4 = 1
    (gdb) print *(a + 0)
    $5 = 1
    (gdb) print a[1]
    $6 = 2
    (gdb) print *(a + 1)
    $7 = 2
    (gdb) print a[2]
    $8 = 3
    (gdb) print *(a + 2)
    $9 = 3

我们已经看到在某些情况下,a 表现的像一个数组,在另一些情况下表现得像一个指向它首元素的指针。接下来会发生什么呢?

答案是当一个数组名在 C 语言表达式中使用时,它“退化”成指向这个数组首元素的指针。这个规则只有两个例外:当数组名传递给 sizeof 函数时,当数组名传递给操作数 & 时。

事实上,a 在传递给操作数 & 时并没有“退化”成一个指针,这就带来一个有趣的问题:由“退化”变成的指针和 &a 存在区别吗?

数值上讲,他们都表示相同的地址:

= preserve do
  :escaped
    (gdb) x/4xb a
    0x7fff5fbff56c: 0x01  0x00  0x00  0x00
    (gdb) x/4xb &a
    0x7fff5fbff56c: 0x01  0x00  0x00  0x00

然而,他们的类型是不同的。我们已经看到 a 退化的值是指向 a首元素的指针;这个必须是类型 int *。对于类型 &a,我们可以直接询问 GDB:

= preserve do
  :escaped
    (gdb) ptype &a
    type = int (*)[3]

从显示上看,&a 是一个指向 3 个整数数组的指针。这就说明:当传递给 & 时,a 没有退化,a 有了一个类型,是 int[3]。

通过测试他们在指针运算时的表现,你可以观察到 a 的退化值和 &a 的明显区别。

= preserve do
  :escaped
    (gdb) print a + 1
    $10 = (int *) 0x7fff5fbff570
    (gdb) print &a + 1
    $11 = (int (*)[3]) 0x7fff5fbff578

注意到对 a 增加 1 等于对 a 的地址增加 4,与此同时,对 &a 增加 1 等于对 a 的地址增加 12!

实际上 a 退化成的指针是 &a[0];

= preserve do
  :escaped
    (gdb) print &a[0]
    $11 = (int *) 0x7fff5fbff56c

结论

希望我已经向你证明 GDB 是学习 C 语言的一个灵巧而有富有探索性的环境。你能使用 print 打印表达式的值,使用 x 查看内存中原始字节,使用 ptype 配合类型系统进行问题修补。

如果你想要进一步对使用 GDB 学习 C 语言进行尝试,我有一些建议如下:

  • 1.用 gdb 通过 Ksplice 指针挑战。
  • 2.研究结构体是怎样在内存中存储的? 他们与数组比较又有什么异同?
  • 3.使用 GDB 的 disassemble 命令学习汇编语言!一个特别有趣的练习是研究函数调用栈是如何工作的。
  • 4.试试 GDB 的 “ tui ”模式,这个模式在常规 GDB 顶层提供一个图像化的 ncurses 层(Ncurses 提供字符终端处理库,包括面板和菜单)。在 OS X 系统中,你可能需要用源代码安装 GDB。

Alan 是 Hacker School 的推广者。他想要感谢 David Albert、Tom Ballinger、Nicholas Bergson-Shilcock 和 Amy Dyer 给予非常有帮助的反馈。

通过 GDB 学习 C 语言,首发于博客 - 伯乐在线

08 Jun 00:31

Java PermGen 去哪里了?

by 张东升

原文链接:原文作者:Monica Beckwith  以下为本人翻译,仅用于交流学习,版权归原作者和InfoQ所有,转载注明出处,请不要用于商业用途

在Java虚拟机(JVM)内部,class文件中包括类的版本、字段、方法、接口等描述信息,还有运行时常量池,用于存放编译器生成的各种字面量和符号引用。

在过去(自定义类加载器还不是很常见的时候),类大多是”static”的,很少被卸载或收集,因此被称为“永久的(Permanent)”。同时,由于类class是JVM实现的一部分,并不是由应用创建的,所以又被认为是“非堆(non-heap)”内存。

在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM由于指针膨胀,默认是85M)。永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误(对于这句话,译者的理解是:32位的JVM默认MaxPermSize是64M,而JDK8里的Metaspace,也可以通过参数-XX:MetaspaceSize 和-XX:MaxMetaspaceSize设定大小,但如果不指定MaxMetaspaceSize的话,Metaspace的大小仅受限于native memory的剩余大小。也就是说永久代的最大空间一定得有个指定值,而如果MaxPermSize指定不当,就会OOM)。

注:在JDK7之前的版本,对于HopSpot JVM,interned-strings存储在永久代(又名PermGen),会导致大量的性能问题和OOM错误。从PermGen移除interned strings的更多信息查看这里

译者注:从JDK7开始永久代的移除工作,贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap。但永久代仍然存在于JDK7,并没有完全的移除:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

在JDK7 update 4即随后的版本中,提供了完整的支持对于Garbage-First(G1)垃圾收集器,以取代在JDK5中发布的CMS收集器。使用G1,PermGen仅仅在FullGC(stop-the-word,STW)时才会被收集。G1仅仅在PermGen满了或者应用分配内存的速度比G1并发垃圾收集速度快的时候才触发FullGC。

而对于CMS收集器,通过开启布尔参数-XX:+CMSClassUnloadingEnabled来并发对PermGen进行收集。对于G1没有类似的选项,G1只能通过FullGC,stop the world,来对PermGen进行收集。

永久代在JDK8中被完全的移除了。所以永久代的参数-XX:PermSize和-XX:MaxPermSize也被移除了。

在JDK8中,classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory。一些新的flags被加入:
-XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集

默认情况下,class metadata的分配仅受限于可用的native memory总量。可以使用MaxMetaspaceSize来限制可为class metadata分配的最大内存。当class metadata的使用的内存达到MetaspaceSize(32位clientVM默认12Mbytes,32位ServerVM默认是16Mbytes)时就会对死亡的类加载器和类进行垃圾收集。设置MetaspaceSize为一个较高的值可以推迟垃圾收集的发生。

Native Heap,就是C-Heap。对于32位的JVM,C-Heap的容量=4G-Java Heap-PermGen;对于64位的JVM,C-Heap的容量=物理服务器的总RAM+虚拟内存-Java Heap-PermGen

这里科普下,在Windows下称为虚拟内存(virtual memory),在Linux下称为交换空间(swap space),用于当系统需要更多的内存资源而物理内存已经满了的情况下,将物理内存中不活跃的页转移到磁盘上的交换空间中。

在JDK8,Native Memory,包括Metaspace和C-Heap。

IBM的J9和Oracle的JRockit(收购BEA公司的JVM)都没有永久代的概念,而Oracle移除HotSpot中的永久代的原因之一是为了与JRockit合并,以充分利用各自的特点。

再见,再见PermGen,你好Metaspace

随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。

类的元数据信息转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。

此外,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。

永久代的移除对最终用户意味着什么?

由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。

注:永久代的移除并不意味者类加载器泄露的问题就没有了。因此,你仍然需要监控你的消费和计划,因为内存泄露会耗尽整个本地内存,导致内存交换(swapping),这样只会变得更糟。

移动到Metaspace和它的内存分配

Metaspace VM利用内存管理技术来管理Metaspace。这使得由不同的垃圾收集器来处理类元数据的工作,现在仅仅由Metaspace VM在Metaspace中通过C++来进行管理。Metaspace背后的一个思想是,类和它的元数据的生命周期是和它的类加载器的生命周期一致的。也就是说,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被释放。

之前我们不严格的使用这个术语“Metaspace”。更正式的,每个类加载器存储区叫做“a metaspace”。这些metaspaces一起总体称为”the Metaspace”。仅仅当类加载器不在存活,被垃圾收集器声明死亡后,该类加载器对应的metaspace空间才可以回收。Metaspace空间没有迁移和压缩。但是元数据会被扫描是否存在Java引用。

Metaspace VM使用一个块分配器(chunking allocator)来管理Metaspace空间的内存分配。块的大小依赖于类加载器的类型。其中有一个全局的可使用的块列表(a global free list of chunks)。当类加载器需要一个块的时候,类加载器从全局块列表中取出一个块,添加到它自己维护的块列表中。当类加载器死亡,它的块将会被释放,归还给全局的块列表。块(chunk)会进一步被划分成blocks,每个block存储一个元数据单元(a unit of metadata)。Chunk中Blocks的分配线性的(pointer bump)。这些chunks被分配在内存映射空间(memory mapped(mmapped) spaces)之外。在一个全局的虚拟内存映射空间(global virtual mmapped spaces)的链表,当任何虚拟空间变为空时,就将该虚拟空间归还回操作系统。

上面这幅图展示了Metaspace使用metachunks在mmapeded virual spaces分配的情形。类加载器1和3描述的是反射或匿名类加载器,使用“特定的”chunk尺寸。类加载器2和4使用小还是中等的chunk尺寸取决于加载的类数量。

Metaspace大小的调整和可以使用的工具

正如前面提到了,Metaspace VM管理Metaspace空间的增长。但有时你会想通过在命令行显示的设置参数-XX:MaxMetaspaceSize来限制Metaspace空间的增长。默认情况下,-XX:MaxMetaspaceSize并没有限制,因此,在技术上,Metaspace的尺寸可以增长到交换空间,而你的本地内存分配将会失败。

对于64位的服务器端JVM,-XX:MetaspaceSize的默认大小是21M。这是初始的限制值(the initial high watermark)。一旦达到这个限制值,FullGC将会被触发进行类卸载(当这些类的类加载器不再存活时),然后这个high watermark被重置。新的high watermark的值依赖于空余Metaspace的容量。如果没有足够的空间被释放,high watermark的值将会上升;如果释放了大量的空间,那么high watermark的值将会下降。如果初始的watermark设置的太低,这个过程将会进行多次。你可以通过垃圾收集日志来显示的查看这个垃圾收集的过程。所以,一般建议在命令行设置一个较大的值给XX:MetaspaceSize来避免初始时的垃圾收集。

每次垃圾收集之后,Metaspace VM会自动的调整high watermark,推迟下一次对Metaspace的垃圾收集。

这两个参数,-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio,类似于GC的FreeRatio参数,可以放在命令行。

针对Metaspace,JDK自带的一些工具做了修改来展示Metaspace的信息:

  • jmap -clstats :打印类加载器的统计信息(取代了在JDK8之前打印类加载器信息的permstat)。一个例子的输出当运行DaCapo’s Avrora基准测试:
$ jmap -clstats <PID>
Attaching to process ID 6476, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.5-b02
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness.liveness analysis may be inaccurate ...
class_loader classes bytes parent_loader alive? type 
<bootstrap\> 655 1222734 null live <internal> 
0x000000074004a6c0000x000000074004a708dead java/util/ResourceBundle$RBClassLoader@0x00000007c0053e20
0x000000074004a76000 null dead sun/misc/Launcher$ExtClassLoader@0x00000007c002d248 0x00000007401189c8 1 1471
0x00000007400752f8dead sun/reflect/DelegatingClassLoader@0x00000007c0009870 0x000000074004a708116 3160530x000000074004a760 dead sun/misc/Launcher$AppClassLoader@0x00000007c0038190 
0x00000007400752f8538 7738540x000000074004a708 dead org/dacapo/harness/DacapoClassLoader@0x00000007c00638b0 
total = 6 1310 2314112 N/A alive=1, dead=5 N/A
  • jstat -gc :Metaspace的信息也会被打印出来,如下面的例子所示:
  • jcmd GC.class_stats:这是一个新的诊断命令,可以使用户连接到存活的JVM,转储Java类元数据的详细统计。

注:在JDK8 build 13下,需要开启参数-XX:+UnlockDiagnosticVMOptions

$ jcmd <PID> help GC.class_stats
9522:
GC.class_stats 
Provide statistics about Java class meta data. Requires -XX:+UnlockDiagnosticVMOptions. 
Impact: High: Depends on Java heap size and content. 
Syntax : GC.class_stats [options] [<columns>] 
Arguments: 
  columns : [optional] Comma-separated list of all the columns to show. If not specified, the following columns are shown: InstBytes,KlassBytes,CpAll,annotations,MethodCount,Bytecodes,MethodAll,ROAll,RWAll,Total (STRING, no default value) 
Options: (options must be specified using the <key> or <key>=<value> syntax) 
  -all : [optional] Show all columns (BOOLEAN, false) 
  -csv : [optional] Print in CSV (comma-separated values) format for spreadsheets (BOOLEAN, false) 
  -help : [optional] Show meaning of all the columns (BOOLEAN, false)

注:对于列的更多信息,请查看这里
一个输出列子:

$ jcmd <PID> GC.class_stats 
7140:
Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName 
1 -1 426416 480 0 0 0 0 0 24 576 600 [C 
2 -1 290136 480 0 0 0 0 0 40 576 616 [Lavrora.arch.legacy.LegacyInstr; 
3 -1 269840 480 0 0 0 0 0 24 576 600 [B 
4 43 137856 648 0 19248 129 4886 25288 16368 30568 46936 java.lang.Class 
5 43 136968 624 0 8760 94 4570 33616 12072 32000 44072 java.lang.String 
6 43 75872 560 0 1296 7 149 1400 880 2680 3560 java.util.HashMap$Node 
7 836 57408 608 0 720 3 69 1480 528 2488 3016 avrora.sim.util.MulticastFSMProbe 
8 43 55488 504 0 680 1 31 440 280 1536 1816 avrora.sim.FiniteStateMachine$State 
9 -1 53712 480 0 0 0 0 0 24 576 600 [Ljava.lang.Object; 
10 -1 49424 480 0 0 0 0 0 24 576 600 [I 
11 -1 49248 480 0 0 0 0 0 24 576 600 [Lavrora.sim.platform.ExternalFlash$Page; 
12 -1 24400 480 0 0 0 0 0 32 576 608 [Ljava.util.HashMap$Node; 
13 394 21408 520 0 600 3 33 1216 432 2080 2512 avrora.sim.AtmelInterpreter$IORegBehavior 
14 727 19800 672 0 968 4 71 1240 664 2472 3136 avrora.arch.legacy.LegacyInstr$MOVW 
…<snipped> 
…<snipped> 
1299 1300 0 608 0 256 1 5 152 104 1024 1128 sun.util.resources.LocaleNamesBundle 
1300 1098 0 608 0 1744 10 290 1808 1176 3208 4384 sun.util.resources.OpenListResourceBundle 
1301 1098 0 616 0 2184 12 395 2200 1480 3800 5280 sun.util.resources.ParallelListResourceBundle 
        2244312 794288 2024 2260976 12801 561882 3135144 1906688 4684704 6591392 Total 
        34.0% 12.1% 0.0% 34.3% - 8.5% 47.6% 28.9% 71.1% 100.0% 
Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName

当前的问题

先前提到的,Metaspace VM使用块分配器(chunking allocator)。chunk的大小取决于类加载器的类型。由于类class并没有一个固定的尺寸,这就存在这样一种可能:可分配的chunk的尺寸和需要的chunk的尺寸不相等,这就会导致内存碎片。Metaspace VM还没有使用压缩技术,所以内存碎片是现在的一个主要关注的问题。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java PermGen 去哪里了?

06 Jun 01:11

垃圾回收器如何处理循环引用

by 技术小黑屋

垃圾回收是一门编程语言中必不可少的一部分,不论是手动释放内存的C和C++,还是自动回收垃圾的Java和C#等语言。对于Java这样的语言,一般的开发者不强求关心对象回收和内存释放,但是理解垃圾回收对开发工作还是大有裨益的。

在编程语言中,普遍存在着循环引用这样的问题,垃圾回收器是如何处理循环引用呢,常用的垃圾回收有引用计数和引用对象遍历两种实现,它们各自又是如何处理循环引用呢?本文讲以JVM中的GC为例逐一回答这些问题。

何为循环引用

如果有两个或者以上的对象,它们彼此引用,就会造成循环引用。如下面的例子

1
2
3
4
5
6
7
8
class Node {
  Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;

代码中,a对象引用了b对象,b对象也引用了a对象,这种情况下a对象和b对象就形成了循环引用。

引用计数GC处理

什么是引用计数

引用计数是一种垃圾回收的形式,每一个对象都会有一个计数来记录有多少指向它的引用。其引用计数会变换如下面的场景

  • 当对象增加一个引用,比如赋值给变量,属性或者传入一个方法,引用计数执行加1运算。
  • 当对象减少一个引用,比如变量离开作用域,属性被赋值为另一个对象引用,属性所在的对象被回收或者之前传入参数的方法返回,引用计数执行减1操作。
  • 当引用计数变为0,代表该对象不被引用,可以标记成垃圾进行回收。

如何处理

实际上单纯的基于引用计数实现的计数器无法处理循环引用带来的问题。

CPython的垃圾回收就是采用引用计数,采用引用计数的主垃圾回收器会清理垃圾,对于那些因为循环引用无法清理的对象,CPython会不时启动一个辅助的基于引用遍历的垃圾回收器来清理它们。

引用遍历GC处理

什么是引用对象遍历

垃圾回收器从被称为GC Roots的点开始遍历遍历对象,凡是可以达到的点都会标记为存活,堆中不可到达的对象都会标记成垃圾,然后被清理掉。 GC Roots有哪些

  • 类,由系统类加载器加载的类。这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用。注意,一般情况下由自定义的类加载器加载的类不能成为GC Roots
  • 线程,存活的线程
  • Java方法中的局部变量或者参数
  • JNI方法栈中的局部变量或者参数
  • JNI全局引用
  • 用做同步监控的对象
  • 被JVM持有的对象,这些对象由于特殊的目的不被GC回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。但是具体有哪些前面提到的对象依赖于具体的JVM实现。

如何处理

基于引用对象遍历的垃圾回收器可以处理循环引用,只要是涉及到的对象不能从GC Roots强引用可到达,垃圾回收器都会进行清理来释放内存。

总结

基于引用计数的垃圾回收器无法处理循环引用导致的内存泄露问题,但是其在主流的JVM中很少,几乎所有的JVM都是采用引用对象遍历的方法,垃圾回收器都会处理循环引用潜在的问题。

一本书

05 Jun 05:40

Spark的速度快是以丧失计算结果正确性为代价的

by changming

是的,Spark很快。但是它不保证它算出的值是对的,哪怕你要做的只是简单的整数累加。

Spark最著名的一篇论文是:《Spark: Cluster Computing with Working Sets》。当你读它的时候你需要明白:文中代码不保证计算结果是正确的。具体来说,它的Logistic Regression的代码在map阶段用到了accumulator。下面解释为什么这么做是错误的。

假设有这样一个简单的任务:

input file的每一行是100个整数,要求竖着加下来

例如:

输入

1 2 3 4 5 ... 100
1 2 3 4 5 ... 200
1 3 3 4 5 ... 100

输出

3 7 9 12 15 ... 400

很简单,对吧?是个猪都会算。 在hadoop上这个问题可以通过Map reduce来解决。首先把输入文件分成N个大小相等的块。然后每个块输出一行100个整数,如 2 4 6 8 10 ... 200
然后reducer接收每个mapper的输出结果,累加起来得到最终结果。

缺点是: 从mapper到reducer是需要DISK-IO及网络传输的。那么需要传输N*100个整数。当输入集的维数很大(每行有上百万个字节)的时候,很浪费。

spark很巧妙的引入了accumulator的概念。同一台机器上所有的task的输出,会先在这个机器上进行本地汇总,然后再发给reducer。这样就不再是task数量*维数,而是机器数量*维数。会节省不少。具体来说,在做机器学习的时候,大家很习惯的用accumulator来做这样的计算。

accumulator是被很careful设计的。比如,只有master节点能读取accumulator的值,worker节点不能。在“Performance and Scalability of Broadcast in Spark
”一文中,作者写到:“Accumulators can be defined for any type that has an “add” operation and a “zero” value. Due to their “add-only” semantics, they are easy to make fault-tolerant.” 。但真的是这样吗?并不是。

accumulator如果不是运行在运算的最后一环,那么正确性无法保证。因为accumulator不是map/reduce函数的输入或输出,accumulator是表达式求值中的side-effect。举个例子:

val acc = sc.accumulator(0)
data.map(x => acc += 1; f(x))
data.count()
// acc should equal data.count() here
data.foreach{...}
// Now, acc = 2 * data.count() because the map() was recomputed.

这个问题被spark的创始人Matei标为Won't Fix。

那么是不是写代码小心点不要触发重复计算就行了呢?也不是。task是有可能fail-retry的,再或者因为某一个task执行的慢,所以同时有它的多个副本在跑。这些都可能会导致accumulator结果不正确。 Accumulators只能用在RDD的actions中,不能用在Transformations。举例来说:可以在reduce函数中用,但是不能在map函数中用。

如果不用accumlators,但又想节省网络传输,那么Matei说:“I would suggest creating fewer tasks. If your input file has a lot of blocks and hence a lot of parallel tasks, you can use CoalescedRDD to create an RDD with fewer blocks from it. ”

意思就是说,那你就把task划分大一点,把task的数量减少。比如每台机器只有1个task。 Downside其实也很明显,任务的执行容易不balance。

参考:https://issues.apache.org/jira/browse/SPARK-732
https://issues.apache.org/jira/browse/SPARK-3628
https://issues.apache.org/jira/browse/SPARK-5490

https://github.com/apache/spark/pull/228

05 Jun 05:39

详解Java中的clone方法 — 原型模式

by importnewzz

Java中对象的创建

clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。那么在java语言中,有几种方式可以创建对象呢?

1 使用new操作符创建一个对象

2 使用clone方法复制一个对象

那么这两种方式有什么相同和不同呢? new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。

复制对象 or 复制引用

在Java中,以下类似的代码非常常见:

Person p = new Person(23, "zhang");
Person p1 = p;

System.out.println(p);
System.out.println(p1);

当Person p1 = p;执行之后, 是创建了一个新的对象吗? 首先看打印结果:

com.pansoft.zhangjg.testclone.Person@2f9ee1ac
com.pansoft.zhangjg.testclone.Person@2f9ee1ac

可已看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p和p1只是引用而已,他们都指向了一个相同的对象Person(23, “zhang”) 。 可以把这种现象叫做引用的复制。 (关于引用和对象的区分,可以参考我之前的文章Java中的String为什么是不可变的? — String源码分析 , 其中有一节讲到了引用和对象的区分)。上面代码执行完成之后, 内存中的情景如下图所示:

而下面的代码是真真正正的克隆了一个对象。

Person p = new Person(23, "zhang");  
Person p1 = (Person) p.clone();  

System.out.println(p);  
System.out.println(p1);

从打印结果可以看出,两个对象的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量:

com.pansoft.zhangjg.testclone.Person@2f9ee1ac
com.pansoft.zhangjg.testclone.Person@67f1fba0

以上代码执行完成后, 内存中的情景如下图所示:

深拷贝 or 浅拷贝

上面的示例代码中,Person中有两个成员变量,分别是name和age, name是String类型, age是int类型。代码非常简单,如下所示:

public class Person implements Cloneable{  

    private int age ;  
    private String name;  

    public Person(int age, String name) {  
        this.age = age;  
        this.name = name;  
    }  

    public Person() {}  

    public int getAge() {  
        return age;  
    }  

    public String getName() {  
        return name;  
    }  

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return (Person)super.clone();  
    }  
}

由于age是基本数据类型, 那么对它的拷贝没有什么疑议,直接将一个4字节的整数值拷贝过来就行。但是name是String类型的, 它只是一个引用, 指向一个真正的String对象,那么对它的拷贝有两种方式: 直接将源对象中的name的引用值拷贝给新对象的name字段, 或者是根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段。这两种拷贝方式分别叫做浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下图所示:

下面通过代码进行验证。如果两个Person对象的name的地址值相同, 说明两个对象的name都指向同一个String对象, 也就是浅拷贝, 而如果两个对象的name的地址值不同, 那么就说明指向不同的String对象, 也就是在拷贝Person对象的时候, 同时拷贝了name引用的String对象, 也就是深拷贝。验证代码如下:

Person p = new Person(23, "zhang");  
Person p1 = (Person) p.clone();  

String result = p.getName() == p1.getName()   
        ? "clone是浅拷贝的" : "clone是深拷贝的";  

System.out.println(result);

打印结果为:

clone是浅拷贝的

所以,clone方法执行的是浅拷贝, 在编写程序时要注意这个细节。

覆盖Object中的clone方法, 实现深拷贝

现在为了要在clone对象时进行深拷贝, 那么就要Clonable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。如果只是用Object中默认的clone方法,是浅拷贝的,再次以下面的代码验证:

static class Body implements Cloneable{  
    public Head head;  

    public Body() {}  

    public Body(Head head) {this.head = head;}  

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  

}  
static class Head /*implements Cloneable*/{  
    public  Face face;  

    public Head() {}  
    public Head(Face face){this.face = face;}  

}   
public static void main(String[] args) throws CloneNotSupportedException {  

    Body body = new Body(new Head());  

    Body body1 = (Body) body.clone();  

    System.out.println("body == body1 : " + (body == body1) );  

    System.out.println("body.head == body1.head : " +  (body.head == body1.head));  

}

在以上代码中, 有两个主要的类, 分别为Body和Face, 在Body类中, 组合了一个Face对象。当对Body对象进行clone时, 它组合的Face对象只进行浅拷贝。打印结果可以验证该结论:

body == body1 : false
body.head == body1.head : true

如果要使Body对象在clone时进行深拷贝, 那么就要在Body的clone方法中,将源对象引用的Head对象也clone一份。

static class Body implements Cloneable{  
    public Head head;  
    public Body() {}  
    public Body(Head head) {this.head = head;}  

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        Body newBody =  (Body) super.clone();  
        newBody.head = (Head) head.clone();  
        return newBody;  
    }  

}  
static class Head implements Cloneable{  
    public  Face face;  

    public Head() {}  
    public Head(Face face){this.face = face;}  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}   
public static void main(String[] args) throws CloneNotSupportedException {  

    Body body = new Body(new Head());  

    Body body1 = (Body) body.clone();  

    System.out.println("body == body1 : " + (body == body1) );  

    System.out.println("body.head == body1.head : " +  (body.head == body1.head));  

}

打印结果为:

body == body1 : false
body.head == body1.head : false

由此可见, body和body1内的head引用指向了不同的Head对象, 也就是说在clone Body对象的同时, 也拷贝了它所引用的Head对象, 进行了深拷贝。

真的是深拷贝吗

由上一节的内容可以得出如下结论:如果想要深拷贝一个对象, 这个对象必须要实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象引用的其他对象也要clone一份 , 这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。

那么,按照上面的结论, Body类组合了Head类, 而Head类组合了Face类,要想深拷贝Body类,必须在Body类的clone方法中将Head类也要拷贝一份,但是在拷贝Head类时,默认执行的是浅拷贝,也就是说Head中组合的Face对象并不会被拷贝。验证代码如下:(这里本来只给出Face类的代码就可以了, 但是为了阅读起来具有连贯性,避免丢失上下文信息, 还是给出整个程序,整个程序也非常简短)

static class Body implements Cloneable{  
    public Head head;  
    public Body() {}  
    public Body(Head head) {this.head = head;}  

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        Body newBody =  (Body) super.clone();  
        newBody.head = (Head) head.clone();  
        return newBody;  
    }  

}  

static class Head implements Cloneable{  
    public  Face face;  

    public Head() {}  
    public Head(Face face){this.face = face;}  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}   

static class Face{}  

public static void main(String[] args) throws CloneNotSupportedException {  

    Body body = new Body(new Head(new Face()));  

    Body body1 = (Body) body.clone();  

    System.out.println("body == body1 : " + (body == body1) );  

    System.out.println("body.head == body1.head : " +  (body.head == body1.head));  

    System.out.println("body.head.face == body1.head.face : " +  (body.head.face == body1.head.face));  

}

打印结果为:

body == body1 : false
body.head == body1.head : false
body.head.face == body1.head.face : true

内存结构图如下图所示:

那么,对Body对象来说,算是这算是深拷贝吗?其实应该算是深拷贝,因为对Body对象内所引用的其他对象(目前只有Head)都进行了拷贝,也就是说两个独立的Body对象内的head引用已经指向了独立的两个Head对象。但是,这对于两个Head对象来说,他们指向了同一个Face对象,这就说明,两个Body对象还是有一定的联系,并没有完全的独立。这应该说是一种不彻底的深拷贝。

如何进行彻底的深拷贝

对于上面的例子来说,怎样才能保证两个Body对象完全独立呢?只要在拷贝Head对象的时候,也将Face对象拷贝一份就可以了。这需要让Face类也实现Cloneable接口,实现clone方法,并且在在Head对象的clone方法中,拷贝它所引用的Face对象。修改的部分代码如下:

static class Head implements Cloneable{  
    public  Face face;  

    public Head() {}  
    public Head(Face face){this.face = face;}  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        //return super.clone();  
        Head newHead = (Head) super.clone();  
        newHead.face = (Face) this.face.clone();  
        return newHead;  
    }  
}   

static class Face implements Cloneable{  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}

再次运行上面的示例,得到的运行结果如下:

body == body1 : false
body.head == body1.head : false
body.head.face == body1.head.face : false

这说名两个Body已经完全独立了,他们间接引用的face对象已经被拷贝,也就是引用了独立的Face对象。内存结构图如下:

依此类推,如果Face对象还引用了其他的对象, 比如说Mouth,如果不经过处理,Body对象拷贝之后还是会通过一级一级的引用,引用到同一个Mouth对象。同理, 如果要让Body在引用链上完全独立, 只能显式的让Mouth对象也被拷贝。

到此,可以得到如下结论:如果在拷贝一个对象时,要想让这个拷贝的对象和源对象完全彼此独立,那么在引用链上的每一级对象都要被显式的拷贝。所以创建彻底的深拷贝是非常麻烦的,尤其是在引用关系非常复杂的情况下, 或者在引用链的某一级上引用了一个第三方的对象, 而这个对象没有实现clone方法, 那么在它之后的所有引用的对象都是被共享的。 举例来说,如果被Head引用的Face类是第三方库中的类,并且没有实现Cloneable接口,那么在Face之后的所有对象都会被拷贝前后的两个Body对象共同引用。假设Face对象内部组合了Mouth对象,并且Mouth对象内部组合了Tooth对象, 内存结构如下图:

写在最后

clone在平时项目的开发中可能用的不是很频繁,但是区分深拷贝和浅拷贝会让我们对java内存结构和运行方式有更深的了解。至于彻底深拷贝,几乎是不可能实现的,原因已经在上一节中进行了说明。深拷贝和彻底深拷贝,在创建不可变对象时,可能对程序有着微妙的影响,可能会决定我们创建的不可变对象是不是真的不可变。clone的一个重要的应用也是用于不可变对象的创建。关于创建不可变对象,我会在后续的文章中进行阐述,敬请期待。

可能感兴趣的文章

04 Jun 11:43

Distributed Systems Are a UX Problem

03 Jun 00:40

我的MYSQL学习心得(1) :简单语法

by scsecrystal

使用MYSQL有一段时间了,由于公司使用SQLSERVER和MYSQL,而且服务器数量和数据库数量都比较多

管理起来比较吃力,在学习MYSQL期间我一直跟SQLSERVER进行对比

第一期主要是学习MYSQL的基本语法,陆续还有第二、第三、第四期,大家敬请期待o(∩_∩)o


语法的差异

我这里主要说语法的不同

1、默认约束

区别:mysql里面DEFAULT关键字后面是不用加括号的

--sqlserver
CREATE TABLE emp
(
id INT DEFAULT(12)
)

--mysql
CREATE TABLE emp
(
id INT DEFAULT 12
)

2、设置自增列

MYSQL的自增列一定要是有索引的列,设置种子值要在表的后面设置

--设置自增列
--sqlserver
CREATE TABLE emp
    (
      id INT IDENTITY(1, 1)
    )

--mysql
-- 设置自增ID从N开始
CREATE TABLE emp (
ID INT  PRIMARY KEY AUTO_INCREMENT
) AUTO_INCREMENT = 100 ; --(设置自增ID从100开始)

设置自增列的步长,可以分为全局级别和会话级别

如果是会话级别,那么当用户新建一个会话的时候,那么步长又回到了全局级别,所以mysql的步长跟sqlserver的步长有很大的不同

mysql不能设置为表级别的步长!!

mysql服务器维护着2种mysql的系统参数(系统变量):全局变量(global variables)和会话变量(session variables)。

它们的含义与区别如其各占的名称所示,session variables是在session级别的,对其的变更只会影响到本session;global variables是系统级别的,

对其的变更会影响所有新session(变更时已经存在session不受影响)至下次mysql server重启动。

注意它的变更影响不能跨重启,要想再mysql server重启时也使用新的值,那么就只有通过在命令行指定变量选项或者更改选项文件来指定,

而通过SET变更是达不到跨重启的。
每一个系统变量都有一个默认值,这个默认值是在编译mysql系统的时候确定的。

对系统变量的指定,一般可以在server启动的时候在命令行指定选项或者通过选项文件来指定

当然,大部分的系统变量,可以在系统的运行时,通过set命令指定其值。

查看系统当前默认的自增列种子值和步长值

SHOW GLOBAL VARIABLES LIKE 'auto_incre%'; -- 全局变量

问:如果有一张表,里面有个字段为id的自增主键,当已经向表里面插入了10条数据之后,删除了id为8,9,10的数据,再把mysql重启,

之后再插入一条数据,那么这条数据的id值应该是多少,是8,还是11?
答:如果表的类型为MyISAM,那么是11。如果表的类型为InnoDB,则id为8。
这是因为两种类型的存储引擎所存储的最大ID记录的方式不同,MyISAM表将最大的ID记录到了数据文件里,重启mysql自增主键的最大ID值也不会丢失;
而InnoDB则是把最大的ID值记录到了内存中,所以重启mysql或者对表进行了OPTIMIZE操作后,最大ID值将会丢失。

顺便说一下MYSQL获取当前表的自增值的四种方法

1、 SELECT MAX(id) FROM person   针对特定表

2、 SELECT LAST_INSERT_ID()  函数   针对任何表

3、 SELECT @@identity    针对任何表

@@identity 是表示的是最近一次向具有identity属性(即自增列)的表插入数据时对应的自增列的值,是系统定义的全局变量。

一般系统定义的全局变量都是以@@开头,用户自定义变量以@开头。

使用@@identity的前提是在进行insert操作后,执行select @@identity的时候连接没有关闭,否则得到的将是NULL值。

4.  SHOW TABLE STATUS LIKE ’person’

如果针对特定表,建议使用这一种方法

得出的结果里边对应表名记录中有个Auto_increment字段,里边有下一个自增ID的数值就是当前该表的最大自增ID.

3、查看表定义

SQLSERVER

EXEC sp_help 'emp'

MYSQL

DESC emp

4、修改表名

修改表名也有差异,将表emp改为emp2

--sqlserver
EXEC sys.[sp_rename] @objname = N'emp', -- nvarchar(1035)
    @newname = 'emp2' -- sysname

--mysql
ALTER TABLE emp RENAME emp2

5、修改字段的数据类型

将id字段的int类型改为bigint

--sqlserver
ALTER TABLE [dbo].[emp2] ALTER COLUMN [ID] BIGINT

--mysql
ALTER TABLE emp2 MODIFY id BIGINT

6、修改字段名

MYSQL里修改字段名的时候需要加上字段的数据类型否则会报错,而CHANGE也可以只修改数据类型,实现和MODIFY同样的效果

方法是将SQL语句中的“新字段名”和“旧字段名”设置为相同的名称,只改变“数据类型”

改变数据类型,例如刚才那个例子,将id列改为bigint数据类型

ALTER TABLE emp2 CHANGE id id BIGINT

修改字段名

--sqlserver
EXEC sys.[sp_rename] @objname = N'emp2.id', -- nvarchar(1035)
    @newname = 'iid', -- sysname
    @objtype = 'column' -- varchar(13)

--mysql
ALTER TABLE emp2 CHANGE id iid BIGINT

7、添加字段

添加字段的语法差不多,但是MYSQL里可以使用FIRSTAFTER关键字指定添加的字段的位置

--sqlserver
ALTER TABLE [dbo].[emp2] ADD NAME NVARCHAR(200) NULL 

--mysql
ALTER TABLE emp2 ADD NAME NVARCHAR(200)  NULL

8、删除字段

MYSQL删除字段不需要添加COLUMN关键字的

--sqlserver
ALTER TABLE [dbo].[emp2] DROP COLUMN NAME 

--mysql
ALTER TABLE emp2 DROP NAME

9、删除外键约束

MYSQL跟SQLSERVER删除约束的方法也有很大的区别

在SQLSERVER里面,无论是唯一约束,check约束还是外键约束都可以使用下面的SQL语句来删除掉

ALTER TABLE 表名 DROP CONSTRAINT 约束名

但是MYSQL里面,如果是外键约束,需要使用 DROP FOREIGN KEY,如果是主键约束需要使用DROP PRIMARY KEY,有点麻烦

--sqlserver
ALTER TABLE dbo.emp2 DROP CONSTRAINT fk_emp_dept

--mysql
--删除外键约束
ALTER TABLE emp2 DROP FOREIGN KEY fk_emp_dept
--删除主键约束
ALTER TABLE emp2 DROP PRIMARY KEY pk_emp_dept

10、删除表

删除表的语法两个都是一样的

--sqlserver
DROP TABLE [dbo].[emp2]

--mysql
DROP TABLE emp2

但是如果要同时删除多个表或者删除之前要先判断一下,MYSQL就方便多了

--sqlserver
IF (OBJECT_ID('dbo.emp2') IS NOT NULL )
DROP TABLE [dbo].[emp2]

--mysql
DROP TABLE IF EXISTS emp1 ,emp2

SQLSERVER需要一张一张表判断,然后一张一张表drop

MYSQL就不一样,语法非常简洁: DROP TABLE IF EXISTS emp1 ,emp2


总结

这篇文章只是简单介绍了一下MYSQL跟SQLSERVER的语法方面的差异

以后会写更多关于MYSQL跟SQLERVER差异的文章,和我这段时间使用MYSQL期间的一些心得,大家敬请期待o(∩_∩)o

如有不对的地方,欢迎大家拍砖o(∩_∩)o 

2014-7-16补充

USE test;
-- myisam引擎
CREATE TABLE TEST(
ID int unsigned not null auto_increment,
name varchar(10) not null,
  key(name,id))engine=MYISAM auto_increment=100
;

-- innodb引擎
CREATE TABLE TESTIdentity(
ID int unsigned   not null auto_increment,
NID INT UNSIGNED ,
name varchar(10) not null,
  key(id))engine=INNODB auto_increment=100
;

--或者主键
CREATE TABLE TESTIdentity(
ID int unsigned   not null auto_increment,
NID INT UNSIGNED ,
name varchar(10) not null,
  key(id))engine=INNODB auto_increment=100
;

[Database4]
ErrorCode: -2147467259, Number: 1075
ErrorMessage: Incorrect table definition; there can be only one auto column and it must be defined as a key

alter table TESTIdentity modify column nid int auto_increment;

无论innodb引擎还是MYISAM引擎的表中,只能有一个自增列,并且自增列一定是索引列,无论是二级索引还是主键索引

这里跟SQLSERVER是不一样,SQLSERVER允许一张表有多个自增列,并且不需要在自增列上创建索引

我的MYSQL学习心得(1) :简单语法,首发于博客 - 伯乐在线

03 Jun 00:20

Rust趋向于稳定,已为生产使用做好了准备

by Jeff Martin

预期的一致,Rust编程语言完成了第一个稳定的发行版本。Rust把自身描述为是一门“兼有低级层面上对性能的控制以及高级层面上的便利性和安全保证”的语言。此次官方1.0版本的一大新闻是体现了对稳定性的关注,这使那些想要学习和使用Rust的人有了一个可依赖的坚实基础。

Rust的新用户将发现这是一个友好的环境,因为Rust编译器不允许使用不稳定的特性。Rust标准库指南包含了Rust语言内置的基本类型、模块和宏的资料信息。除了特别指出的地方以外,其余都被认为是稳定的。每夜构建版本仍可以运行开发者想尝试的不稳定特性。

开发者Chris Morgan在制作的网站上提供了一个关于准备以完全的纯Rust 栈制作web应用的进展分析报表。Rust项目还单独维护了一本关于Rust入门及实践应用的在线手册

作为项目计划的一部分,每6周需完成一次代码定期出仓,Rust 1.1 Beta版随此次发行版本一同释出。(可在Rust主页上获取二进制文件和源文件。)完整的发行说明连同源文件都可从GItHub中获取。

查看英文原文:Rust Achieves Stability, Ready for Production Use


感谢张龙对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

02 Jun 11:05

什么是垃圾回收

本文摘自我们几周后即将出版的Garbage Collection Handbook一书的样章。同时也让你能熟悉下垃圾回收的基础知识——这选自该书的第一章。

乍一看,垃圾回收所做的事情应当恰如其名——查找并清除垃圾。事实上却恰恰相反。垃圾回收会跟踪所有仍在使用的对象,然后将剩余的对象标记为垃圾。牢记了这点之后,我们再来深入地了解下这个被称为“垃圾回收”的自动化内存回收在JVM中到底是如何实现的。

手动管理内存

在介绍现代版的垃圾回收之前,我们先来简单地回顾下需要手动地显式分配及释放内存的那些日子。如果你忘了去释放内存,那么这块内存就无法重用了。这块内存被占有了却没被使用。这种场景被称之为内存泄露

下面是用C写的一个手动管理内存的简单例子:

int send_request() {
    size_t n = read_size();
    int *elements = malloc(n * sizeof(int));

    if(read_elements(n, elements) < n) {
        // elements not freed!
        return -1;
    }

    // …

    free(elements)
    return 0;
}

可以看到,你很容易就会忘了释放内存。内存泄露曾经是个非常普遍的问题。你只能通过不断地修复自己的代码来与它们进行抗争。因此,需要有一种更优雅的方式来自动释放无用内存,以便减少人为错误的可能性。这种自动化过程又被称为垃圾回收(简称GC)。

智能指针

自动垃圾回收早期的一种实现便是引用计数。你知晓每一个对象被引用了几次,当计数器归0的时候,这个对象就可以被安全地回收掉了。C++的共享指针就是一个非常著名的例子:

int send_request() {
    size_t n = read_size();
    stared_ptr<vector<int>> elements 
              = make_shared<vector<int>&gt();

    if(read_elements(n, elements) < n) {
        return -1;
    }

    return 0;
}

我们使用的sharedptr会记录这个对象被引用的次数。如果你将它传递给别人则计数加一,当它离开了作用域后便会减一。一旦这个计数为0,sharedptr会自动地删除底层对应的vector。当然这只是个示例,因为也有读者指出来了,这个在现实中是不太可能出现的,但作为演示是足够了。

自动内存管理

在上面的C++代码中,我们还得显式地声明我们需要使用内存管理。那如果所有的对象都采用这个机制会怎样呢?那简直就太方便了,这样开发人员便无需考虑清理内存的事情了。运行时会自动知晓哪些内存不再使用了,然后释放掉它。也就是说,它自动地回收了这些垃圾。第一代的垃圾回收器是1959年Lisp引入的,这项技术迄今为止一直在不断演进。

引用计数

刚才我们用C++的共享指针所演示的想法可以应用到所有的对象上来。许多语言比如说Perl, Python以及PHP,采用的都是这种方式。这个通过一张图可以很容易说明:

image

绿色的云代表的是程序中仍在使用的对象。从技术层面上来说,这有点像是正在执行的某个方法里面的局部变量,亦或是静态变量之类的。不同编程语言的情况可能会不一样,因此这并不是我们关注的重点。

蓝色的圆圈代表的是内存中的对象,可以看到有多少对象引用了它们。灰色圆圈的对象是已经没有任何人引用的了。因此,它们属于垃圾对象,可以被垃圾回收器清理掉。

看起来还不错对吧?没错,不过这里存在着一个重大的缺陷。很容易会出现一些孤立的环,它们中的对象都不在任何域内,但彼此却互相引用导致引用数不为0。下面便是一个例子:

image

看到了吧,红色部分其实就是应用程序不再使用的垃圾对象。由于引用计数的缺陷,因此会存在内存泄露。

有几种方法可以解决这一问题,比如说使用特殊的“弱”引用,或者使用一个特殊的算法回收循环引用。之前提到的Perl,Python以及PHP等语言,都是使用类似的方法来回收循环引用的,不过这已经超出本文讲述的范围了。我们准备详细介绍下JVM所采用的方法。

标记删除

首先,JVM对于对象可达性的定义要明确一些。它可不像前面那样用绿色的云便含糊了事的,而是有着非常明确及具体的垃圾回收根对象(Garbage Collection Roots)的定义:

  • 局部变量
  • 活动线程
  • 静态字段
  • JNI引用
  • 其它(后面将会讨论到)

JVM通过标记删除的算法来记录所有可达(存活)对象,同时确保不可达对象的那些内存能够被重用。这包含两个步骤:

  • 标记是指遍历所有可达对象,然后在本地内存中记录这些对象的信息
  • 删除会确保不可达对象的内存地址可以在下一次内存分配中使用。

JVM中的不同GC算法,比如说Parallel Scavenge,Parallel Mark+Copy, CMS都是这一算法的不同实现,只是各阶段略有不同而已,从概念上来讲仍然是对应着上面所说的那两个步骤。

这种实现最重要的就是不会再出现泄露的对象环了:

image

缺点就是应用程序的线程需要被暂停才能完成回收,如果引用一直在变的话你是无法进行计数的。这个应用程序被暂停以便JVM可以收拾家务的情况又被称为Stop The World pause(STW)。这种暂停被触发的可能性有很多,不过垃圾回收应该是最常见的一种。

原创文章转载请注明出处:什么是垃圾回收

英文原文链接

01 Jun 11:48

Mesos, omega, borg: a survey

01 Jun 04:25

C for high level programmers (slides)

31 May 10:33

Simple C++11 metaprogramming

31 May 10:33

SSD: How to Optimize Your Solid State Drive for Linux

31 May 00:38

在什么情况下Java比C++快?

by hackingwu

回复者:Cameron Purdy,Oracle中间件高级工程师。

这是根据我同时使用C++和Java工作超过20年所学到的,其实使用Java比C++还要早几年:

1、根据我的经验,当你把优化过的C++代码转换成Java代码,代码的速度会慢大约三倍。

2、根据我的经验,把Java代码转换成C++的代码,速度同样也会慢三倍。首先,这种比较根本没有意义,除非你能意识到用Java的方式去写Java代码,而不是像C++开发者那样去组织C++代码。

3、对于并发的数据结构使用Java往往更有效率。当数据结构不是并发使用时,JVM会消除内存屏障和同步,并倾向使用基于运行时性能分析的并发管理。

4、Java的动态内存管理往往更有效率,在过度使用动态内存管理和多线程的系统中效果尤其明显。

5、Java内联代码往往表现更加优秀,除非你基于分析器对C++代码进行了大量优化(或者确切地知道如何使用内联让C++更加高效……你肯定会爱上这些头文件!)。

6、使用Java的大型项目往往更容易优化,因为JVM为开发人员做了许多“全局”优化(比如内联动态加载的代码能力)。

一家之言,至少我的感觉是这样……

相关文章

27 May 03:40

Metascala:用Scala编写的Java虚拟机(JVM)

by 小编辑

Metascala的目标是搭建一个JVM试验平台,一个Scala编写的3000行JVM可与C/C++ 10万行代码媲美,从而弥补了HotSpot,标准实现,更增补了像延续、隔离或者之类这样有趣功能的实现。代码包括了:字节码解释器,字节码翻译器等等功能。(@网路冷眼 分享)

The post Metascala:用Scala编写的Java虚拟机(JVM) appeared first on 头条 - 伯乐在线.

27 May 03:39

λ表达式之争:Scala vs Java8

by paddx

最近几年Lambda表达式风靡于编程界。很多现代编程语言都把它作为函数式编程的基本组成部分。基于JVM的编程语言如Scala、Groovy及Clojure把它作为关键部分集成在语言中。而如今,(最终)Java 8也加入了这个有趣的行列。

Lambda表达式最有意思的地方在于,在JVM的角度来看它是完全不可见的。在JVM中没有匿名函数或Lambda表达式的概念。JVM唯一知道是字节码。字节码是一个严格的OO规范。由语言的创造者和编译者通过这些限制来创建新的、高级的语言元素。

我们第一次遇到Lambda表达式是需要在Takipi中增加对Scala的支持,所以不得不深入了解Scala的编译器。而这时Java 8也正处在关键时刻。我猜想Scala和Java编译器对Lambda表达式的实现肯定会非常有趣。结果让我极为惊讶。

为了演示这些内容,我写了一个简单的Lambda表达式,功能是将一个字符串列表转换为它们长度的列表。

Java:

List names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

Scala:

val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

不要被它表面的简单所迷惑,后面执行了相当复杂的过程。

我们从Scala开始

代码

我使用 javap 来查看通过Scala编译器生成的.class文件的字节码的内容。让我们看一下字节码的结果(这才是JVM真正执行的内容)。

//将变量名加载到栈中(JVM视为变量#2),先保存在这,之后会在map函数中用到
aload_2

接下来的事情就变得更有趣了,一个由编译器生成的synthetic的实例创建并初始化(译者注:Synthetic类是指由JVM运行时生成的类)。非常有意思的是,Lambda作为整个方法的一部分来定义的,但它实际上完全存在于我们类的外部。

new myLambdas/Lambda1$$anonfun$1 //实例化Lambda对象
dup //把它加入栈中
//最后,调用构造函数.记住,这是源自JVM的一个简单对象
invokespecial myLambdas/Lambda1$$anonfun$1/()V
//这个两行加载immutable.List CanBuildFrom工厂,该工厂能生成新的list。工厂模式是Scala的集合架构的一部分。
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;

//现在,栈上已经有了Lambda对象及工厂,下一阶段就可以调用map函数。
//你应该还记得,我们在一开始的时候将名称变量加载到了栈中。我们现在可以用它来实现map方法的调用了。
//map方法接受一个Lambda对象和一个工厂,生成一个长度的list。

invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

但是请稍等,Lambda对象内部做了什么事情?

Lambda对象

Lambda类来继承自scala.runtime.AbstractFunction1。通过这种方式,map() 函数可以多态调用重写后的 apply() 方法,apply()代码如下:

//这段代码是加载this及目标对象,检测它是不是一个字符串,然后调用另一个重载后的、真正工作的apply方法,最后包装返回结果
aload_0//加载this
aload_1//加载字符串参数
checkcast java/lang/String//确保是一个字符串 - 得到一个Object

// 调用synthetic类的apply()方法
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I

//包装结果
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

真正的执行.length() 操作的代码嵌套在另个一apply方法中,该方法正如我们期望的一样,简单的返回了字符串的长度。

唷……,走了好长的一段路才到这。

aload_1
invokevirtual java/lang/String/length()I
ireturn

我们在上面只是写了一行简单的代码,但是却产生了许多的字节码,包括一个额外的类和一堆方法。但是,这绝不是在劝阻我们不要用Lambda(我们是在Scala中写代码,而不是C)。这仅仅是为了展示这种结构后面的复杂性。

我相当期待Java 8也是用这种方式实现的,但是令人惊讶的时,java采取了完全不同的方式。

Java 8:一种新的方式

Java 8产生的字节码比较短,但是还有更令人惊讶的东西。它刚开始简单的加载了名称变量,然后调用 stream() 方法,但是接下做了一些非常好的优化。它没有创建一个新的对象来包装Lambda函数,而是使用了新的 invokeDynamic 指令,该指令是Java 7时增加的,这个地方的用于调用真实的Lambda函数。

aload_1 // 加载名称变量
//调用stream()方法
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;
//invokeDynamic指令魔法!
invokedynamic #0:apply:()Ljava/util/function/Function;
//调用map()方法
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

InvokeDynamic魔法:这条JVM指令在Java 7中增加,用于减少JVM的限制,允许动态语言在运行时绑定符号。而在这之前,所有的链接都是静态的,在代码编译的时候就由JVM完成。

动态链接:如果你看过invokedynamic指令,你会发现没有引用指向真正的Lambda函数(即lambda$0)。答案归结于invokedynamic指令的设计,但是更简短的答案是Lambda表达式的签名,就我们的例子来说是

//一个名为lamda$0的函数,获取一个字符串,返回一个整数
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

存储在.class的一个单独的表中,该表作为#0参数传递给指令。这个新的表确实改变了字节码规范的结构,这是多年之后的第一次改变,这同样需要采取Takipi的错误分析引擎。

Lambda代码

这段代码是真正的Lambda表达式。非常容易,简单地加载字符串参数,调用length()方法并包装成结果。请注意,它是编译成了一个静态函数,避免像之前看到的Scala一样,传入额外的this对象。

aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn

这是invokedynamic方式的另一个优点,它允许我们通过多态的方式来调用 map() 函数,且不需要包装对象或调用虚拟的的重写方法。非常酷!

总结

Java看起非常具有吸引力,最“严格”的现代语言现在开始使用动态链接来增加Lambda表达式的功能。该方式也是非常有效的一种方式,不需要加载和编译额外的类,Lambda方法只是我们类中一个简单的私有方法。

Java 8确实对Java 7引入的新的技术做了很多优化,使用了非常直接的方式实现了对Lambda表达式的支持。非常高兴能看到像Java这样“端庄”的女士能教我们一些戏法。

相关文章

26 May 00:28

New C++ experimental feature: The tadpole operators

26 May 00:21

为何从10开始到99连续相乘会得到0?

by 李 璟

原文链接  译者: 李璟(jlee381344197@gmail.com)

这是一块非常简单的Java代码片段:

public class HelloWorld{

    public static void main(String []args){

        int product = 1;

        for (int i = 10; i <= 99; i++) {

            product *= i;

        }

        System.out.println(product);

    }

}

为什么得出的结果是0呢?

问题现象

蛋疼的同学可能会发现这个程序执行的规律:
1 * 10 = 10

10 * 11 = 110

110 * 12 = 1320

1320 * 13 = 17160

17160 * 14 = 240240

240240 * 15 = 3603600

3603600 * 16 = 57657600

57657600 * 17 = 980179200

……

 

-1342177280 * 40 = -2147483648

-2147483648 * 41 = -2147483648

-2147483648 * 42 = 0

0 * 43 = 0

0 * 44 = 0

……

0 * 97 = 0

0 * 98 = 0
程序从42开始就已经输出0,所以42以后的数字相乘的结果就显而易见了。从结果中发现,乘积的符号已一种难以理解的方式变换着,表明乘积已经溢出了,同时也说明Java并不会理会整数的上下溢出。

问题解答

请记住Java的int类型是32位的有符号二进制补码表示的数字类型(译者注:64为jdk同样如此)。这是每一步乘法在计算机内部所做的操作:

标注(1)是实际十进制结果。

标注(2)十六进制以及十进制的内部表示结果,int类型只会存储低32位的数据。

标注(3)是标注(2)的补码形式。

如果你好奇0从哪里来,请仔细看上方2进制表示的结果。细心的同学会注意到:

任何一个数与偶数相乘得偶数。

偶数与偶数相乘,会将2进制位整体左移,0从右边填补空位。

偶数与奇数相乘,不会改变最右方0的数量。

当乘法执行的足够多次时,右方的0位会越来越多。最终,连续乘到42时,乘积的2进制表示的低32位全是0,所以int将会是0。

问题扩展

既然知道了问题的原因,我们换一种变量来做同样的操作,以byte为例。

Java的byte变量是8位的有符号数,同样也是补码表示。从上方结果表格看出,连续从10乘到16时,2进制结果的低8位全都是0,所以此时的byte变量是0。而连续乘到15时,低8位是10010000,还记得怎么由补码求原码吗?很简单, 符号位不变,其余位取反加1,得出11110000,既-112,感兴趣的朋友请在自己机器上验证结果。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: 为何从10开始到99连续相乘会得到0?

26 May 00:20

Cling旨在提供一款高性能的C++ REPL

by Sergio De Simone

Cling是一款交互式C++解释器,以LLVM和Clang为基础构建,其目标是通过超越编码-编译-运行-调试这个惯常的C++工作流程提供生产力的飞跃。

Cling提供了一个读取-求值-输出循环(REPL),类似常见的Unix shell,并支持Emacs绑定。使用Cling可以测试C++代码片段,而不需要创建文件、包含头文件等等。使用REPL的主要好处是可以在极短的时间内测试一个想法,而不需要等待构建系统编译代码。REPL在学习一门语言时也非常有用,因为它让试用语言特性变得更简单。

ROOT是Cern的数据分析框架,Cling即是由该框架背后的团队开发完成,作为现有的命令行C/C++解释器CINT的一个替代方案。目前,在粒子物理学领域中,许多实验中都用到了ROOT,包括大型“强子对撞器(Large Hadron Collider)”。

Cling可以解析Clang所能解析的一切内容,并且还支持一些CINT特有的C++扩展。ROOT开发团队列举了Cling提供的主要好处,其中包括使用生产级解析器、JIT允许不使用封装器直接进行库调用、使用独立的解析器和执行引擎。

Cling在GitHub上开源。用户既可以使用每日构建的二进制包进行安装,也可以从源代码构建。官方的一体化构建脚本支持基于Unix的系统,而Windows上的手动构建过程可以借助CMake实现。此外,Gallagher Pryor介绍了针对ARM平台构建Cling的步骤,这比针对x86平台进行构建要复杂得多,因为开发团队没有在他们的构建脚本中直接提供这种支持选项。

查看英文原文:Cling Aims to Provide a High-performance C++ REPL

25 May 13:45

通过JVM日志来进行安全点分析

by -之诸暇

原文链接 作者: Plumbr 译者:之诸暇

许多事件都可能会导致JVM暂停所有的应用线程。这类暂停又被称为”stop-the-world”(STW)暂停。触发STW暂停最常见的原因就是垃圾回收了(github中的一个例子),但不同的JIT活动(例子),偏向锁擦除(例子),特定的JVMTI操作,以及许多场景也可能会导致应用程序暂停。

应用程序线程可以被安全地停止掉的那个时间点,就叫做安全点。这一术语也通常用来指代SWT暂停。

通常来讲GC日志都是打开的。然而,并非所有安全点的信息都能完整地记录下来。想获取到完整的日志,可以使用下列的JVM选项:


-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

从参数名字来看你可能会觉得是与GC相关的,其实不然——打开这些选项能够记录下所有的安全点,而不止是GC暂停的。如果你用上述的选项来运行下这个例子(github源码

你会在标准输出中看到如下信息:


Application time: 0.3440086 seconds
Total time for which application threads were stopped: 0.0620105 seconds
Application time: 0.2100691 seconds
Total time for which application threads were stopped: 0.0890223 seconds


很通俗易懂(和GC日志相比来说)——从中你可以得知应用程序在前344毫秒中是在处理实际工作的,然后将所有线程暂停了62毫秒,紧接着又工作了210ms,然后又暂停了89ms。

你还可以将这些选项与GC的选项结合起来使用,比如将上面这个程序加上-XX:+PrintGCDetails 选项后再运行一次,输出则变成这样了;


[Full GC (Ergonomics) [PSYoungGen: 1375253K->0K(1387008K)] [ParOldGen: 2796146K->2049K(1784832K)] 4171400K->2049K(3171840K), [Metaspace: 3134K->3134K(1056768K)], 0.0571841 secs] [Times: user=0.02 sys=0.04, real=0.06 secs] 
Total time for which application threads were stopped: 0.0572646 seconds, Stopping threads took: 0.0000088 seconds

综上可知,应用线程被强制暂停了57ms来进行垃圾回收。其中又有8ms是用来等待所有的应用线程都到达安全点。如果我们用同样的选项运行另一个例子(Github源码)的话,输出又变成这样的了:


Total time for which application threads were stopped: 0.0001273 seconds, Stopping threads took: 0.0000196 seconds
Total time for which application threads were stopped: 0.0000648 seconds, Stopping threads took: 0.0000174 seconds

光从这些信息我们无从得知是什么导致的暂停,因为看不出有任何的垃圾回收的活动。如果你想更详细地了解安全点的信息的话,可以使用这组JVM参数:


-XX:+PrintSafepointStatistics  -XX:PrintSafepointStatisticsCount=1

启用这些参数使得JVM会将一些额外的信息记录到标准输出中,大概类似这样:


5.141: RevokeBias                       [      13          0              2    ]      [     0     0     0     0     0    ]  0  
Total time for which application threads were stopped: 0.0000782 seconds, Stopping threads took: 0.0000269 seconds

关于安全点的信息是按照如下的顺序进行显示的:

– JVM启动之后所经历的毫秒数(上例中是5.141)
– 触发这次暂停的操作名(RevokeBias)。
如果你看见”no vm operation”,就说明这是一个”保证安全点”。JVM默认每秒会触发一次安全点来处理那些非紧急的排队的操作。GuaranteedSafepointInterval选项可以用来调整这一行为(设置为0的话就会禁用该功能)
– 停在安全点的线程的数量(13)
– 在安全点开始时仍在运行的线程的数量(0)
– 虚拟机操作开始执行前仍处于阻塞状态的线程的数量(2)
– 到达安全点时的各个阶段以及执行操作所花的时间(0)

因此我们可以看出,使用了偏向锁会导致大量的STW暂停,尽管它们只花了几十毫秒。在如今这个大量使用并发的年代,禁用它们也不是什么罕见的事情。

不管怎样,多打印些日志总会减少一些麻烦事的。你可以使用如下的JVM参数:


-XX:+LogVMOutput -XX:LogFile=vm.log

所有的虚拟机日志都会输出到vm.log文件中。如何解读这些日志并做出响应是一个很大的课题,这已经远超本文所讨论的范围了,不过未来我仍会更新一到两篇文章来讲下这个,请拭目以待.

英文原文链接

本文同时发表在我的个人博客:Java译站

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: 通过JVM日志来进行安全点分析

25 May 11:24

视频:42 分钟讲解 42 个 IntelliJ IDEA 使用技巧

by aoi

视频:42 分钟讲解 42 个 IntelliJ IDEA 使用技巧

The post 视频:42 分钟讲解 42 个 IntelliJ IDEA 使用技巧 appeared first on 头条 - 伯乐在线.

24 May 09:27

Zapcc: A faster C++ compiler

23 May 00:28

Velocity官方指南-容器

by 曾道涛

原文网址  译者:曾道涛

简介
“容器”这一概念对于Velocity来说很重要,它是在系统的各部分之间传递一系列数据的通用技术。也就是说,容器是Java层(或者程序员)和模板层(或者设计师)之间的数据搬运工。作为程序员,你会收集各种类型的对象,包括所有你程序需要的,然后把它们放在容器里。对于设计师来说,这些对象,以及它们的方法和属性,可以通过被称为引用的模板元素来访问。一般来说,你会和设计师一起决定应用程序的数据需求。从某种意义上说,一旦你为设计师生成了一个数据集,也即在模板中提供了“API”访问。因此,在这个阶段的开发过程中,你值得花时间仔细分析。

虽然Velocity允许你创建自己的容器类来满足特殊的需求和技术(比如像一个直接访问LDAP服务器的容器),一个叫VelocityContext的基本实现类已经作为发行版的一部分提供给你。

VelocityContext适合所有的一般需求,所以我们强烈推荐你使用它。只有在特殊情况和高级应用中,才需要你扩展或者创建你自己的容器实现。

使用VelocityContext就像使用一个普通的Java Hashtable类一样简单。虽然接口包含其他有用的方法,我们使用的两个主要方法是:

public Object put(String key, Object value);
public Object get(String key);

请注意,参数value必须继承自java.lang.Object。基本类型像int和float必须用合适的包装类包装。
这就是所有关于容器的基本操作。如需更多信息,可以查看发行版中包含的API文档。

 

for和foreach()遍历对象的支持

作为一个程序员,对于你放在容器中的对象有很大的自主权。但是正如大多数自主权,这也需要一点点责任,所以要理解Velocity支持什么,以及任何可能出现的问题。Velocity支持几种集合类型在VTL中使用foreach()语句。
•  Object []   普通对象数组,在这里无需多说。如果一个类中提供了迭代器接口,Velocity会自动包装你的数组,但是这不应该涉及到程序员和模板的设计者。更有趣的是,Velocity现在允许模板设计者把数组当作定长链表来处理(Velocity 1.6中就是这样)。这意味着他们既可以在数组上也可以在java.util.List的实例上调用像size(), isEmpty()和get(int)这样的方法,而无需关心它们本身的差异。
•  java.util.Collection  Velocity通过iterator()方法返回一个迭代器在循环中使用,所以如果你正在你的对象上实现一个集合接口,请确保iterator()方法返回一个有效的迭代器。
•  java.util.Map    这里,Velocity通过接口的values()方法返回一个Collection接口,iterator()方法在它上面调用来检索用于循环的迭代器。
•  java.util.Iterator   使用的时候需要注意:目前只是暂时支持,关注的问题是迭代器不能重置。如果一个未初始化的迭代器被放进了容器,并且在多个foreach()语句中使用,如果第一个foreach()失败了,后面的都会被阻塞,因为迭代器不会重启。
•  java.util.Enumeration     使用的时候需要注意:和java.util.Iterator一样,目前只是暂时支持,关注的问题是枚举不能重置。如果一个未初始化的枚举被放进了容器,并且在多个foreach()语句中使用,如果第一个foreach()失败了,后面的都会被阻塞,因为枚举不会重启。

•  任何拥有public Iterator iterator()方法的公有类永远不会返回null。Velocity会把寻找一个iterator()方法作为最后的手段。这提供了很大的灵活性,也自动支持Java 1.5中的java.util.Iterable接口。

对于Iterator和Enumeration,推荐只有在万不得已的情况下才把它们放进容器,你也应该尽可能地让Velocity找到合适的、可复用的迭代接口。

虽然有充足的理由直接使用java.util.Iterator接口(比如像JDBC这样的大数据集),但是如果能够避免,用其他的可能会更好。“直接”是说像下面这样:

Vector v = new Vector();
v.addElement("Hello");
v.addElement("There");

context.put("words", v.iterator() );

当迭代器本身被放进了容器。当然,你也可以简单地这样做:

context.put("words", v );

两种方式都可以:Velocity能够识别实现了Collection(像List)接口的Vector,并因此找到iterator()方法,在它每次需要的时候调用iterator()来刷新迭代器。一旦Velocity在foreach()中使用了一个空迭代器(先跳过这里…),Velocity没办法为它正在使用的下一个foreach()获取一个新的迭代器。结果是后面任何使用空迭代器引用的foreach()都会阻塞,并且没有输出。

上面这些并不是为了表明在Velocity中遍历集合需要十分小心。恰恰相反,一般来说,只要在你向容器中添加迭代器时小心就可以了。

对静态类的支持
并非所有的类都可以实例化。像java.lang.Math这样的类不提供任何公有的构造函数,但是它包含了有用的静态方法。为了从模板中访问这些静态方法,你可以简单地把这些类自身添加到容器中:

context.put("Math", Math.class);

这样你就可以在模板中用$Math引用调用java.lang.Math中的任何公有静态方法。

容器链

Velocity容器设计的一大创新特性是容器链的概念。有时也被称为contextwrapping,这个高级特性允许你以一种方式把独立的容器连接在一起,使它们对模板来说像一个连续的容器。

最好用一个例子来说明:

VelocityContext context1 = new VelocityContext();

context1.put("name","Velocity");
context1.put("project", "Jakarta");
context1.put("duplicate", "I am in context1");

VelocityContext context2 = new VelocityContext( context1 );

context2.put("lang", "Java" );
context2.put("duplicate", "I am in context2");

template.merge( context2, writer );

在上面这段代码中,我们创建context2,使它连接context1。这意味着在模板中,你可以访问这两个VelocityContext对象中放置的任意项,只要在添加对象的时候没有重复的键。如果遇到这种情况,就像上面的键”duplicate”,最近的容器中存储的对象可以被访问。在上面这个例子中,返回的对象是字符串”I am in context2″。

注意,这种容器中对象的重复或者“覆盖”,不会以任何方式损坏或者修改被覆盖的对象。所以在上面的例子中, 字符串”I am in context1″还存在并且完好无损,仍然可以通过context1.get(“duplicate”)访问。但是在上面的例子中,模板中引用”$duplicate”的值会是”I am in context2″,模板不能访问被覆盖的字符串”I am in context1″。

也要注意,当你使用模板向一个渲染之后再检查的容器中添加信息的时候,你必须要小心。在一个模板中,通过set()语句改变容器只会影响外层的容器。所以在期望模板中的数据已经被添加到内部容器时,请确保你没有丢弃外层的容器。

这个特性有很多用途,目前最常用的就是提供层次数据访问和工具箱。

就像前面提到过的,Velocity容器机制是可以扩展的,但是超出了本指南的当前范围。如果你有兴趣,可以查看包org.apache.velocity.context中的类,看看提供的容器怎么组合在一起。更进一步,有几个例子在发行版的examples/context_example目录下,它们展示了可能的实现,包括一个使用数据库作为后台存储的例子。

请注意,这些例子不被支持,它们仅仅是出于示范和教学目的。

模板中创建的对象
在模板中,有两种情况Java代码必须处理运行时创建的对象:

当模板设计者通过Java代码调用容器中放置的对象的方法:
#set($myarr = [“a”,”b”,”c”] )
$foo.bar( $myarr )

当模板向容器中添加对象,Java代码可以在合并过程完成后访问这些对象:
#set($myarr = [“a”,”b”,”c”] )
#set( $foo = 1 )
#set( $bar = “bar”)

如果非常直接地处理这些情况,有几个事情需要知道:
•  当放在容器中或者传给方法时,VTL RangeOperator [ 1..10 ]和ObjectArray [“a”,”b”]是java.util.ArrayList对象。所以,当你设计接受模板中创建的数组的方法时,你应该牢记这个问题。
•  VTL Map引用毫无疑问是用java.util.Map存储的。
•  小数在容器中会是Doubles或者BigDecimals,整数会是 Integer, Long,或者BigIntegers字符串当然还是Strings。
•  Velocity在调用方法时会适当地省略参数,所以调用setFoo( int i )把一个整数放入容器,#set()和它等价。

其他容器问题
VelocityContext(或者任何派生自AbstractContext的Context)提供的一个特性是节点特有的自我监视缓存。一般来说,作为一个开发者,当你使用VelocityContext作为你的容器时,不必担心这些。但是,有一个目前已知的使用模式,你必须知道这个特性。

当VelocityContex访问模板中的节点时,它会收集关于这些有序节点的自我监视信息。所以,在下面这几种情况:
•  你使用同一个VelocityContext对象遍历同样的模板。
•  模板缓存关闭。
•  在每次迭代中,你通过getTemplate()请求模板。

你的VelocityContext有可能出现内存泄露(在收集更多自我监视信息时真会出现)。真正发生的是,它为每个它访问的模板收集模板节点的自我监视信息,当模板缓存关闭时,对VelocityContext来说它每次都是访问一个新模板。因此,它收集更多的自我监视信息,并且一直增长。强烈推荐你做以下一条或者更多条:

•  通过模板补偿处理,每次遍历都新建一个VelocityContext。这样就不会收集自我监视缓存数据。在你由于VelocityContext已经填充了数据或对象而想重用它的情况下,你可以简单地把填充好的VelocityContext包装成另一个VelocityContext,外层的那个会收集自我监视信息,你直接丢弃就可以了。例如 VelocityContext useThis = new VelocityContext( populatedVC );它可以很好地工作,因为外层的容器会存储自我监视缓存数据,它可以从内部容器获取任何请求的数据(当它为空时)。你仍然要注意,如果你的模板把数据放入容器并且期待后面的遍历使用这些数据,你需要做另外一个准备,因为任何模板set()语句会被存储在最外层的容器。看“Context chaining”章节中的讨论来获取更多信息。
•  打开模板缓存。这样可以避免在每次遍历中重复解析模板,因此VelocityContext不仅可以避免增加自我监视的缓存信息,还可以带来性能的提升。
•  在循环遍历的过程中重用模板对象。如果缓存关闭了,你不必让Velocity一遍又一遍地重复读取和解析相同的模板,所以VelocityContext就不会每次都收集新的自我监视信息。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Velocity官方指南-容器