线程安全的Hashtable + synchronizedMap源码剖析

文章目录

      • 1. Hashtable
        • 1.1 字段
        • 1.2 构造方法
        • 1.3 get方法
        • 1.4 put方法
        • 1.3 rehash方法
        • 1.5 安全性
        • 1.6 实例
      • 2. synchronizedMap
        • 2.1 字段、构造方法
        • 2.2 get和put方法


前面已经学习了HashMap的底层实现原理,并且从源码的角度详细的分析了其中的属性字段、构造方法、常用的get和put操作,以及扩容机制。HashMap作为一个最为常用的Map接口的实现类,一个广为人知的特点就是:线程不安全。多线程之间的运行可能会导致底层形成循环链表,或者造成数据的丢失和覆盖。如果想要使用一种线程安全的Map结构,那么可以选择synchronizedMap、HashTable和ConcurrentHashMap,下面分别从源码剖析一下synchronizedMap和Hashtable的源码实现。


1. Hashtable

Hashtable与HashMap都是用来存储key-value类型数据,在正式理解Hashtable的实现前,首先看一下它和hashMap的区别之处,带着疑问看源码总是更好的。两者的主要区别如下:

  • Hashtable的key和value都不允许为null,而HashMap两者都不做限制

  • Hashtable线程安全,hashMap做不到线程安全

  • HashMap使用iterator进行迭代遍历,它是一种fail-fast迭代器;而Hashtable使用enumeration迭代器,它属于fail-fast迭代器

    Java中的fail-fast和fast-safe机制

  • HashTable继承了Dictionary类,hashMap继承了AbstractMap类

下面依然从字段、构造方法和常用方法三个方面,对Hashtable的源码进行解读。

1.1 字段

Hashtable中定义的属性字段如下所示:

// 继承父类Dictionary
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
	
    // 不可序列化的Entry类型的table,用于存储数据
    private transient Entry<?,?>[] table;
	// Hashtable中数据数量
    private transient int count;
	// 扩容阈值
    private int threshold;
	// 加载因子
    private float loadFactor;
	// 修改次数
    private transient int modCount = 0;
	// 序列化ID
    private static final long serialVersionUID = 1421746759512286392L;
    // 设置table的最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    // 默认存储结构
    private transient volatile Set<K> keySet;
    private transient volatile Set<Map.Entry<K,V>> entrySet;
    private transient volatile Collection<V> values;
}

属性字段基本上和HashMap中相差不多,不过并没有直接设置初始化容量大小。

1.2 构造方法

Hahstable同样提供了四个构造方法,如下所示:

// 全参构造
public Hashtable(int initialCapacity, float loadFactor) {
    // 如果初始容量小于0直接抛异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
   	// 如果加载因子小于0,或者为null,同样直接抛异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);
	// 如果设置初始容量0中,则取1,保证table至少能存储一个数据
    if (initialCapacity==0)
        initialCapacity = 1;
    // 初始化加载因子
    this.loadFactor = loadFactor;
    // 初始化table
    table = new Entry<?,?>[initialCapacity];
    // 设置扩容阈值,如果超过了设置的MAX_ARRAY_SIZE就不能再扩容了
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

// 自定义初始化容量,加载因子默认为0.75
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

// 无参构造,容量初始化默认为11,加载因子为0.75
public Hashtable() {
    this(11, 0.75f);
}

// 使用已有的Map实现初始化Hashtable
public Hashtable(Map<? extends K, ? extends V> t) {
    // 如果map的大小小于11,则设置初始容量为11,否则设置为已有map的大小为初始容量
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

1.3 get方法

首先,看一下get()的执行过程,源码如下所示:

// 线程安全通过在方法上使用synchronized关键字实现
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    // 首先获取key对应的哈希值
    int hash = key.hashCode();
    // 计算key在table中的索引
    // 取模
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 如果对应的桶存在链表,则遍历链表查找key对应的元素
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        // 如果链表中某个节点的哈希值相同并相等,返回对应的value
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    // 否则返回null
    return null;
}

get()整体的逻辑还是很简单的,主要就是先找key在table中位置,然后遍历可能存在的链表进行查找。

1.4 put方法

put()的源码如下所示:

public synchronized V put(K key, V value) {
    // 如果value为null,直接抛空指针异常
    if (value == null) {
        throw new NullPointerException();
    }

    // 首先根据key获取它的哈希值
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    // 计算key在table中的索引
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // 获取table中对应位置的链表
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            // 如果key已经存在,则覆盖旧的value
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
	// 否则直接添加到相应的位置上
    addEntry(hash, key, value, index);
    return null;
}

addEntry完成key不在table时的put操作 ,源码如下:

private void addEntry(int hash, K key, V value, int index) {
    // 修改次数增1
    modCount++;

    Entry<?,?> tab[] = table;
    // 如果此时table中的元素数量已经大于或等于阈值,则需要进行扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();
		
        // 重新计算key在新table中的索引位置
        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // 新建一个Entry对象,将其放在链表的头部
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

这里使用的Entry的构造函数如下:

protected Object clone() {
    return new Entry<>(hash, key, value,
                       (next==null ? null : (Entry<K,V>) next.clone()));
}

因此,可以看到Hashtable在put操作中使用的是头插法。如果table给定的index位置没有插入元素,则直接将其放在index位置,那么它的next指针指向null;否则将其插入到链表的头部,它的next指针指向之前的头部元素。

1.3 rehash方法

rehash()对应Hashtable的扩容机制,它的实现源码如下所示:

protected void rehash() {
    // 首先获取扩容前table的大小
    int oldCapacity = table.length;
    // 保存旧的table
    Entry<?,?>[] oldMap = table;

    // 新的容量为旧容量 * 2 + 1
    int newCapacity = (oldCapacity << 1) + 1;
    // 如果扩容后容量超过了设置的MAX_ARRAY_SIZE,并且旧容量已经是MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            return;
        // 那么新的容量只能是MAX_ARRAY_SIZE
        newCapacity = MAX_ARRAY_SIZE;
    }
    // 创建新的table
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
	// 修改次数加1
    modCount++;
    // 重新计算扩容阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    // 更新table
    table = newMap;
	// 将旧table中元素按照新的index放置到新table中
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            // 这里使用的仍然是头插法
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

从扩容的源码实现中可以看出,Hashtable扩容后的容量大小有两种情况:

  • 如果超过了MAX_ARRAY_SIZE,那么新的容量大小也只能是MAX_ARRAY_SIZE
  • 否则,新容量是之前容量的2倍加1,并没有规则一定是2的倍数

而且,在将旧的table中元素重新放入新table时使用的依然是头插法。

1.5 安全性

HashTable是一个线程安全的哈希表,它通过使用synchronized关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程环境中效率低下等问题。

public synchronized int size(){}
public synchronized boolean isEmpty() {}
public synchronized Enumeration<K> keys() {}
public synchronized Enumeration<V> elements() {}
public synchronized boolean contains(Object value) {}
public synchronized boolean containsKey(Object key) {}
public synchronized V get(Object key) {}
public synchronized V put(K key, V value) {}
public synchronized V remove(Object key) {}
public synchronized void putAll(Map<? extends K, ? extends V> t) {}
public synchronized void clear() {}
public synchronized Object clone() {}
public synchronized String toString() {}
public synchronized boolean equals(Object o) {}
public synchronized int hashCode() {}

1.6 实例

public class HashtableDemo {
    public static void main(String[] args) {
        Hashtable<Integer, String> map = new Hashtable<>();
        map.put(2, "Ball");
        map.put(23, "James");
        map.put(24, "Kobe");
        map.put(11, "Yao");
        map.put(1, "Forlogen");
		
        // 四种遍历方式
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.println("key = " + entry.getKey() + " value = " + entry.getValue());
        }
        System.out.println("--------------------");

        Set<Integer> set = map.keySet();
        for (Integer key : set) {
            System.out.println("key = " + key + " value = " + map.get(key));
        }
        System.out.println("--------------------");

        Enumeration e = map.elements();
        while(e.hasMoreElements()){
            System.out.println(e.nextElement());
        }
        System.out.println("--------------------");

        map.forEach((k, v) -> System.out.println("key = " + k + " value = " + v));

    }
}

2. synchronizedMap

2.1 字段、构造方法

Colletions工具类中的synchronizedMap可以用于创建线程安全的Map集合,它的字段定义和构造方法源码如下所示:

// 实现了Map接口
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;
	// 被final修饰的普通Map对象
    private final Map<K,V> m;     // Backing Map
    // 互斥锁对象
    final Object mutex;        // Object on which to synchronize
	
    // 不指定锁对象
    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        // 不指定锁对象时,锁对象就是SynchronizedMap本身
        mutex = this;
    }	
	// 自定义锁对象
    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        // 否则,锁对象为传入的mutex
        this.mutex = mutex;
    }

2.2 get和put方法

get()put()的源码如下:

public V get(Object key) {
    synchronized (mutex) {return m.get(key);}
}

public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

可以看到,方法使用的仍然是一般Map中的get()put(),不过在真正执行前需要synchronized进行加锁,只有获得了锁对象的操作才能操作Map。其他方法的实现同样需要synchronized进行加锁,详细内容可自行走读SynchronizedMap源码。

你可能感兴趣的:(Java)