非线程安全类(ArrayList和HashMap)

1、ArrayList

(1)实现List接口,底层数组实现。初始容量为10,每一次扩容是上一次容量的1.5倍。

需要注意的是,size是按照调用add,remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1。

(2)源码分析

添加:

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 判断是否需要扩容,Increments modCount!!
        elementData[size++] = e;
        return true;

    }

//向指定位置插入元素

public void add(int index, E element) {
        rangeCheckForAdd(index);//判断索引是否越界
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,  //将指定位置之后的所有元素向后移动一个位置
                         size - index);
        elementData[index] = element;//将元素插入指定位置
        size++;//ArrayList大小加1
      

    }

//移除指定位置的元素

public E remove(int index) {
        rangeCheck(index);//判断是否越界


        modCount++;//修改次数加1
        E oldValue = elementData(index);


        int numMoved = size - index - 1;//因为index是从0开始
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,//将要删除元素之后的元素向前移动
                             numMoved);
        elementData[--size] = null; // Let gc do its work,将后一个元素置为null


        return oldValue;

    }

//删除指定位置的元素

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;

    }

 private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }

 

扩容

我们看一下,构造ArrayList的时候,默认的底层数组大小是10:

 /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this(10);
    }

那么有一个问题来了,底层数组的大小不够了怎么办?答案就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,源码如下:

 

private void ensureCapacityInternal(int minCapacity) {
        modCount++;//ArrayList被修改的次数
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)  //minCapacity为现在ArrayList真实大小,elementData为现在ArrayList                                                                                  //   最    大  容 量,如果minCaaopacity》elementData,则需要扩容
            grow(minCapacity); //扩容
  }

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)//如果扩容后,容量还是小,放不下已有元素,则将容量置为所需要的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);//将旧数组拷贝到扩容后的新数组
    }

注://Arrays.copyOf函数, 基本数据类型:int、boolean、char、double 等
public static int[] copyOf(int[] original, int newLength) {    
    int[] copy = new int[newLength];    //新创建一个数组
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));    
    return copy;    
}
 

获取:

//得到指定位置的元素

public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
  }

 

 

修改:

 

//修改指定位置的元素

public E set(int index, E element) {

        rangeCheck(index);


        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;

    }

(3)ArrayList的优缺点

从上面的几个过程总结一下ArrayList的优缺点。ArrayList的优点如下:
1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
不过ArrayList的缺点也十分明显:
1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
因此,ArrayList比较适合顺序添加、随机访问的场景



(4)ArrayList和Vector的区别

I:ArrayList不是线程安全的,Vector是线程安全的。

II:ArrayList和Vector的扩容机制不一样。

ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:

List<String> synchronizedList = Collections.synchronizedList(list);

synchronizedList.add("aaa");

synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++){     

        System.out.println(synchronizedList.get(i));

}

 

Vector可以指定增长因子,如果该增长量指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长量;如果不指定增长量,那么就给原数组大小*2,源代码是这样的:


int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

 

(5)为什么ArrayList的elementData是用transient修饰的?

们看一下ArrayList中的数组,是这么定义的:


private transient Object[] elementData;

看到ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData的大小为10,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

 

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();


        // Write out array length
        s.writeInt(elementData.length);


        // Write out all elements in the proper order.
        for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);


        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }


    }

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,然后遍历elementData,只序列化那些有的元素,这样:
1、加快了序列化的速度
2、减小了序列化之后的文件大小

 

(5)自定义ArrayList

public class MyArrayList{
private Object [] objs = new Object[10];
private int index = 0;
/**
* 添加
* @param o
*/
public void add(Object o){
if(index == objs.length){
//扩容
extend();
}
objs[index++] = o;
}


/**
* 扩容
*/
private void extend(){
Arrays.copyOf(objs,objs.length+objs.length/2);//新创建一个长度为原数组1.5倍的数组,
                                              //将原数组中的元素拷贝进新数组中
}


/**
* 获取元素个数
*/
public Object get(int index){
return objs[index];
}


/**
* 获得ArrayList的大小,即size
*/
public int size(){
int size = 0;
for(int i = 0;i<objs.length;i++){
if(objs[i] != null){
size++;
}
}
return size;
}


/**
* 清空ArrayList
*/
public void clear(){
for(int i = 0;i<objs.length;i++){
objs[i] = null;
}

}


/**
* 判断是否包含
*/
public boolean contains(Object o){
for(int i=0;i<objs.length;i++){
if(o!=null && o.equals(objs[i])){
return true;
}

}
return false;
}


/**
* 根据索引删除(代码有些问题)
*/
public  void remove(int index){
if(index<0 || index>=size()){
return;
}
if(index == size()-1){
objs[index] = null;
}else{
for(int i=index;i<objs.length;i++){
objs[i] = objs[i+1];
}
}
}
}

 

2、HashMap

(1)底层为数组加链表实现,初始容量为16,扩容因子是0.75,每一次扩容都是上一次容量的2倍。

(2)源码分析

从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

 

源码如下:

 

Java代码  收藏代码

  1. /** 
  2.  * The table, resized as necessary. Length MUST Always be a power of two. 
  3.  */  
  4. transient Entry[] table;  
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {  
  7.     final K key;  
  8.     V value;  
  9.     Entry<K,V> next;  
  10.     final int hash; 
  11.      /**
          * Creates new entry.
          */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;//将当前Entry对象设置为头结点,next指向原来的头结点
        } 

           
  12.     ……  
  13. }  

可以看出,Entry就是数组中的元素,每个 Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

 

存储:

 

Java代码  收藏代码

  1. public V put(K key, V value) {  
  2.     // HashMap允许存放null键和null值。  
  3.     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
  4.     if (key == null)  
  5.         return putForNullKey(value);  
  6.     // 根据key的keyCode重新计算hash值。  
  7.     int hash = hash(key.hashCode());  
  8.     // 搜索指定hash值在对应table中的索引。  
  9.     int i = indexFor(hash, table.length);  
  10.     // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。  
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  //如果key已经存在,则用新值代替老值, 
  12.                                                             //并返回老值
  13.         Object k;  
  14.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  15.             V oldValue = e.value;  
  16.             e.value = value;  
  17.             e.recordAccess(this);  
  18.             return oldValue;  
  19.         }  
  20.     }  
  21.     // 如果i索引处的Entry为null,表明此处还没有Entry。  
  22.     modCount++;  
  23.     // 将key、value添加到i索引处。  
  24.     addEntry(hash, key, value, i);  
  25.     return null;  
  26. }  
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     // 获取指定 bucketIndex 索引处的 Entry   
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  6.     // 如果 Map 中的 key-value 对的数量超过了极限  
  7.     if (size++ >= threshold)  
  8.     // 把 table 对象的长度扩充到原来的2倍。  
  9.         resize(2 * table.length);  
  10. }  

 

从上面的源代码中可以看出:当我们往HashMapput元素的时候,先根据keyhashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),(1)如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。(2)如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。(3)如果要添加的key已经存在,则用新的value代替旧的value,并返回旧的value。

我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

 对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:

 

Java代码  收藏代码

  1. static int indexFor(int h, int length) {  
  2.     return h & (length-1);  
  3. }  

在被模的位数为2的n次方时,用位与代替效率低下的模运算。位与效率相比模运算效率更高。

 

例:15%4=3,代替为 15 & 3=1111 & 0011=0011=3

 

例:
两个key,调用Object的hash方法后值分别为:
32,64,然后entry数组大小为:16,即在调用indexFor时参数分别为[32,15],[64,15],
这时分别对它们调用indexFor方法:
32计算过程:
  100000 & 1111 =>  000000 =>0
64计算过程如下:

 1000000 & 1111 =>  000000 =>0

 

可以看到indexFor在Entry数组大小不是很大时只会对低位进行与运算操作,高位值不参与运算(如果Entry大小为32,则只会与低5位进行与操作),很容易发生hash冲突。

这里,32与64这两个hash值,都被存储在Entry数组0的位置上。

为了解决这个问题,HashMap在做indexFor操作前,需要调用hash方法,使hash值的位值在高低位上尽量分布均匀,hash方法:
static int hash(int h) {  
    // This function ensures that hashCodes that differ only by  
    // constant multiples at each bit position have a bounded  
    // number of collisions (approximately 8 at default load factor).  
    h ^= (h >>> 20) ^ (h >>> 12);  
    return h ^ (h >>> 7) ^ (h >>> 4);  


还是按前面的key,经过Object的hash方法后,分别为32,64来进行运算:
32调用hash运算过程如下:
   原始h为32的二进制: 
        100000
        h>>>20:  
        000000
       h>>>12:
        000000
    
接着运算 h^(h>>>20)^(h>>>12):
    结果:    100000

然后运算: h^(h>>>7)^(h>>>4),
过程如下:
    h>>>7:    000000
    h>>>4:    000010
最后运算: h^(h>>>7)^(h>>>4),
    结果:    100010,即十进制34
    
    调用indexFor方法:
        100010 & 1111 => 2,即存放在Entry数组下标2的位置上
------------------------------------

64的运算结果为:1000100,十进制值为68
    调用indexfor方法:
        1000100 & 1111 => 4,即存放在Entry数组下标4的位置上

 

 

可以看到经过hash方法后,再调用indexFor方法,这样可以减少冲突。

 

 

 

读取:

 

Java代码  收藏代码

  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.         e != null;  
  7.         e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;  
  11.     }  
  12.     return null;  
  13. }  

 HashMapget元素时,(1)首先计算keyhashCode,找到数组中对应位置的某一元素,(2)然后通过keyequals方法在对应位置的链表中找到需要的元素。

 

 

HashMap的resize(rehash)(重新建立一个容量更大的数组):

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }


        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

 

 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

   当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

删除:

 public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);

 }

final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }


        return e;

 }

删除元素,即改变链表的指针即可。

  Fail-Fast机制:

   我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

   这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

 

 在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,此时抛出ConcurrentModificationException()。

   注意到modCount声明为volatile,保证线程之间修改的可见性。

 

注:HashTable 的键和值都允许为null,只允许一个key为null,允许对个value为null。

get方法中:

  1. if (key == null)  
  2.         return getForNullKey();  


put方法中:

 

  1. if (key == null)  
  2.         return putForNullKey(value);  

 

(3)HashMap为什么不是线程安全的?

I、在hashmap做put操作的时候。现在假如A线程和B线程同时向同一个数组位置插入Entry对象,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

II、 当多个线程进行put操作时,当多个线程同时检测到元素的个数超过阈值的时候就会同时调用resize操作,可能会发生条件竞争(竞态条件:多个线程并发访问和操作同一数据且执行结果与访问的特定顺序有关)。因为如果两个线程都发现hashmap需要resize()了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中元素的顺序会反过来。因为移动到新的位置的时候,hashmap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。(否则,针对key的hashcode相同的entry每次添加还要定位到尾结点)。如果条件竞争发生了,可能会出现环形链表。之后,当我们调用get(key)操作时就可能会发生死循环。

假设 线程2 在执行到Entry<K,V> next = e.next;之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点

这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:

 

 

 

 

 

这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;


执行之后的引用关系如下图:

 

变量e又重新指回节点a,将a的next为b,只能继续执行循环体,
Entry<K,V> next = e.next; //null

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];//将a.next = b;

newTable[i] = e; //a

e = next; //null,循环终止

 

 

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值