线程安全:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
常见的容器类
线程安全类:Vector和Hashtable
线程非安全类:ArrayList和HashMap
HashMap实例:
public class MainClass {
public static Map<String, String> hashMap = new HashMap<String, String>();
public static void main(String[] args) throws InterruptedException {
// hashMap = Collections.synchronizedMap(hashMap);
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 25; i++) {
hashMap.put(String.valueOf(i), String.valueOf(i));
}
}
};
Thread t2 = new Thread() {
public void run() {
for (int j = 0; j < 25; j++) {
hashMap.put(String.valueOf(j), String.valueOf(j));
}
}
};
t1.start();
t2.start();
Thread.sleep(1000);
for (int i = 0; i < 25; i++) {
System.err.println(i + ":" + hashMap.get(String.valueOf(i)));
}
}
}
打印的结果不稳定,经常出现错误的值
0:null
1:1
2:2
3:null
4:4
5:null
原因分析:
public V put(K key, V value) {
...
addEntry(hash, key, value, i);
...
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
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];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
从代码中,可以看到,如果发现哈希表的大小超过阀值threshold,就会调用resize方法,扩大容量为原来的两倍,而扩大容量的做法是新建一个Entry[]。
如果在默认情况下,一个HashMap的容量为16,加载因子为0.75,那么阀值就是12,所以在往HashMap中put的值到达12时,它将自动扩容两倍,如果两个线程同时遇到HashMap的大小达到12的倍数时,就很有可能会出现在将oldTable转移到newTable的过程中遇到问题,从而导致最终的HashMap的值存储异常。
JDK1.0引入了第一个关联的集合类HashTable,它是线程安全的。HashTable的所有方法都是同步的。
JDK2.0引入了HashMap,它提供了一个不同步的基类和一个同步的包装器synchronizedMap。synchronizedMap被称为有条件的线程安全类。
JDK5.0util.concurrent包中引入对Map线程安全的实现ConcurrentHashMap,比起synchronizedMap,它提供了更高的灵活性。同时进行的读和写操作都可以并发地执行。
ArrayList实例:
public static List<String> list = new ArrayList<String>();
public static void main(String[] args) throws InterruptedException {
// list = Collections.synchronizedList(list);
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 25; i++) {
list.add(String.valueOf(i));
}
}
};
Thread t2 = new Thread() {
public void run() {
for (int j = 25; j < 50; j++) {
list.add(String.valueOf(j));
}
}
};
t1.start();
t2.start();
Thread.sleep(1000);
for (int i = 0; i < 50; i++) {
System.err.println(i + ":" + list.get(i));
}
}
结果出现异常:
44:48
45:49
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 46, Size: 46
at java.util.ArrayList.RangeCheck(ArrayList.java:547)
at java.util.ArrayList.get(ArrayList.java:322)
at map.Test.main(Test.java:37)
原因分析:
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
在多线程的操作下导致扩容和赋值操作的不统一导致了出错。
util.concurrent包也提供了一个线程安全的ArrayList替代者CopyOnWriteArrayList。
Vector和Hashtable在各自的方法上都加了synchronized关键字来保持同步。
我们往往把同步的概念仅仅的理解为一种互斥的方式。其实同步不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证进入同步方法或同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。