《Java高并发程序设计》学习 --2.8 隐蔽的错误

来源:互联网 发布:淘宝 清明上河图 编辑:程序博客网 时间:2024/05/21 12:00
1)无提示的错误案例
int v1 = 1073741827;
int v2 = 1431655768;
int ave = (v1+v2)/2;
System.out.println(ave);
上述代码中,视图计算v1和v2的均值。这是一个典型的溢出问题。v1+v2的结果已经导致了int的溢出。

2)并发下的ArrayList
ArrayList是一个线程不安全的容器。
public class ArrayListMultiThread {
static ArrayList<Integer> al = new ArrayList<Integer>();
public static class AddThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
al.add(i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddThread());
Thread t2 = new Thread(new AddThread());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(al.size());
}
}
执行这段代码,可能出现三种结果。
第一,程序正常结束。
第二,程序抛出异常:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 6246
这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另外一个线程访问到了不一致的内部状态,导致出现越界问题。
第三,出现了一个非常隐蔽的错误比如打印如下值作为结果:1425166
这是由于多线程访问冲突,使得彼此保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一个位置进行赋值导致的。

3)并发下诡异的HashMap
HashMap同样不是线程安全的。代码如下:
import java.util.HashMap;
import java.util.Map;
public class HashMapMultiThread {
static Map<String, String> map = new HashMap<String, String>();
public static class AddThread implements Runnable {
int start = 0;
public AddThread(int start) {
this.start = start;
}
@Override
public void run() {
for (int i = start; i < 100000; i+=2) {
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.size());
}
}
第一,程序正常结束,并且结果也是符合预期的。HashMap的大小为100000。
第二,程序正常结束,但结果不符合预期,而是一个小于100000的数字。
第三,程序永远无法结束。
前两种情况,和ArrayList的情况非常相似。而第三种情况,通过查看HashMap.put()方法,可知,由于多线程的冲突,这个链表结构已经遭到破坏,链表成环了,下述的迭代就等同于一个死循环。但这个死循环的问题在JDK8中已经不存在了。由于JDK8对HashMap的内部做了大规模调整,规避了这个问题。但即使这样,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案就是使用ConcurrentHashMap代替HashMap。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

4)错误的加锁
假设我们需要一个计数器,这个计数器会被多个线程同时访问。为了确保数据正确性,我们需要对计数器加锁。代码如下:
public class BadLockInteger implements Runnable {
public static Integer i = 0;
static BadLockInteger instance = new BadLockInteger();
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized (i) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果我们得到了一个比20000000小很多的数字。要解释这个问题,得从Integer说起。在Java中,Integer属于不变对象。也就是说对象一旦被创建,就不可能被修改。i++在真实执行时变成了:
i = Integer.valueOf(i.intValue()+1)。进一步查看 Integer.valueOf():
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer.valueOf()实际上是一个工厂方法,它会倾向于返回一个代表指定数值的Integer实例。因此,i++的本质是,创建一个新的Integer对象,并将它的引用赋值给i。
如此一来,我们就明白问题所在,由于在多个线程间,并不一定能够看到同一个对象(因为i对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。


注:本篇博客内容摘自《Java高并发程序设计》

0 0
原创粉丝点击