如果没有实现以上两条(特别是第 1 条),在使用 Java 某些集合时(特别是 HashSet 和 HashMap),你将得不到想要的结果,甚至造成严重的内存泄漏问题。要搞明白里面的原由,我们还得从 HashMap 的内部实现开始说起。
HashMap 在存储键值对(Key-Value Pair)时,首先调用 Key 对象的 hashCode() 方法得到一个数字,这个数字对应了该键值对在 HashMap 中的存放位置,每个位置上不仅存放了 Value,还存放了 Key,即键值对(如下图所示)。我们将存放键值对的位置叫做一个 Bucket(中文名为" 桶 ",即用来装东西的容器,很形象哈),Bucket 中维护了一个 LinkedList(下图中的 Entries),该 LinkedList 用于存放实际的键值对本身。因此,要通过 Key 来获取到相应的 Value,Java 只需要再次调用 Key 的 hashCode() 方法得到该键值对在 HashMap 中的位置,便可以准确快速地获取到 Key 对应的 Value。因此,即便用于存放的 Key 和用于获取的 Key 是相等的,如果他们的 hashCode 不等,那么在获取时便得不到先前存放的准确位置,进而得不到正确的结果。另外,如果 Key 对象的类没有实现 equals() 方法,那么默认情况下 Java 将使用对象的引用地址来判断两个对象的相等性, 而之后我们又很难再次创建出一个和先前引用地址相同的对象,因此便可能出现永远也获取不到 Value 的情况,从而导致内存泄漏。进而我们得到另一个结论:如果一个类的对象将被用于 HashMap 的 Key 或者被直接放入 Set 集合中,那么这个类应该实现 equals() 方法和 hashCode() 方法。
注意到上图中的 152 号 Bucket 了吗?我们发现同时有 John Smith 和 Sandra Dee 两个 Key 都指向了这个相同的 Bucket,即这两个对象的 hashCode 相等。这便是 equals() 和 hashCode() 契约关系的第 2 点。Java 采用了 LinkedList 来存放所有 Key 的 hashCode 相等的键值对。此时在 LinkedList 中,前一个键值对维护了一个指针指向了下一个键值对。当之后通过 John Smith 来获取 Value 时,Java 首先发现其对应的 Bucket 中存在着两个键值对,然后 Java 分别将各个键值对中 Key 对象与所传来的 Key 对象相比,如果相等则返回该键值对所对应的 Value。由此,我们也知道 HashMap 中为什么需要同时存 Key 和 Value 而不是只存 Value 的原因;同时也知道了契约第 2 点的来由。事实上,我们可以让一个类的所有对象都返回相同的 hashCode,只是此时如果该类的对象作为 Key 时,所有的键值对都将存放都相同的 Bucket 位置,每次在获取的时候都需要做多次的比较,因此会影响 HashMap 的获取速度。
线程安全性
通常来说,在 Java 中可以通过两种方式来实现线程安全,一种是通过 Java 自带的并发管控手段(比如使用 syncronized 关键字),另一中是通过创建不可变(Immutable)对象。
在很早的时候,Java 里面有 Vector 和 HashTable 两个集合对象,他们通过使用 syncronized 关键字实现了线程安全,但是同时也暴露出了很大的性能问题。因此现在基本上没有人使用了。为了解决 Vector 和 HashTable 的性能问题,Java 从 1.2 引入的 Collections 框架采用了线程不安全的类,比如 ArrayList 和 HashMap 都是线程不安全的。当然,为了性能而牺牲了线程安全性也是不可取的,因此 Java 通过 Fail-Fast 的 Iterator 来避免多个线程同时操作集合所带的线程冲突问题。比如,当一个线程正在遍历一个集合而另一个线程正在修改该集合时,前者将抛出 ConcurrentModificationException。这当然也不是万全的办法。
另一方面,Java 其实也提供了线程安全的封装类(Wrapper)来实现集合的线程安全性,我们可以通过:
- Collections.synchronizedXXX(collection)
来创建线程安全的集合,这里的 XXX 可以是 Collection、List、Map 和 Set 等。对于一个常规的 colleciton 对象,调用 (synchronizedXXX) 方法将得到封装后的线程安全性。
集合封装类同样采用了 syncronized 关键字来达到线程安全性,并且是在整个集合类上上锁,这样也会带来严重的性能问题。为了解决这样的问题,从 Java 5 开始引入了并发集合 (Concurrent Collections),他们要么采用 Immutable 集合,要么采用更加精细的锁控制来达到线程安全的目的,同时又能保证很高的性能。
并发集合主要包含三类,一是 Copy-On-Write 集合,二是 Compare-And-Swap 集合,三是采用特殊锁的并发集合。Copy-On-Write 集合底层维护的是一个不变的(Immutable)的数组,通过在写(Write)入集合时重新复制(Copy)一份新的集合来达到线程安全,进而得名 Copy-On-Write。Coppy-On-Write 集合包括有 CopyOnWriteArrayList 和 CopyOnWriteArraySet 等。Compare-And-Swap 集合在进行更新的时候,首先维护一个本地拷贝,当执行更新时,比较本地拷贝与原值,如果值相等,则证明在这段时间内还没有其他线程修改原值,此时立即更新;如果不相等,则重新拷贝原值,再计算,再更新,这样也到了线程安全的目的。Compare-And-Swap 集合包括 ConcurrentLinkedQueue 和 ConcurrentSkipListMap 等。第三类是使用特殊锁的集合,这种集合类并不在整个集合类上上锁,而是通过在 Bucket 级别上上锁,从而达到了对并发的更精细的控制,减少了线程的等待时间,从而提高了并发性能。
除了提供并发控制机制外,Java 还提供了不可修改的(unmodifiable)集合来保证线程安全性,可以通过:
- Collections.unmodifiableXXX(collection)
来创建不同 unmodifiable 集合。这样的集合其实也是一个封装类,对于传入的正常集合 collection,通过对 add() 等方法抛出 UnsupportedOperationException 异常来达到不可修改的目的。但是,这样的集合其实并不达到不可修改目的,因为被其包装的 collection 本身依然是可以修改的。
为了实现更好的集合不变性,Guava 类库提供了很多 Immutable 的集合,这些集合是真正不变的。
来源: http://www.cnblogs.com/davenkin/p/java-collections.html