zoukankan      html  css  js  c++  java
  • 第2章 Java并行程序基础(三)

    2.8 程序中的幽灵:隐蔽的错误

    2.8.1 无提示的错误案例

    • 以求两个整数的平均值为例。请看下面代码:
    int v1 = 1073741827;
    int v2 = 1431655768;
    System.out.println("v1 = " + v1);
    System.out.println("v2 = " + v2);
    int ave = (v1 + v2) / 2;
    System.out.println("ave = " + ave);
    
    • 输出如下:
    v1 = 1073741827
    v2 = 1431655768
    ave = -894784850
    
    • 这是一个典型的溢出问题!显然,v1 + v2的结果已经导致了int的溢出。

    2.8.2 并发下的ArrayList

    • ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错。试看下面的代码:
    public class ArrayListMultiThread {
        static ArrayList<Integer> al = new ArrayList<Integer>(10);
        public static class AddThread implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; 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());
        }
    }
    
    • 如果执行这段代码,可能会得到三种结果。
      • 第一,程序正常结束,ArrayList的最终大小确实2000000。这说明即使并行程序有问题,也未必会每次都表现出来。
      • 第二,程序抛出异常:
    Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException:22
    
    • 这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另外一个线程访问到了不一致的内部状态,导致出现越界问题。
      • 第三,出现了一个非常隐蔽的错误,比如打印如下值作为ArrayList的大小:
    1793758
    
    • 显然,这是由于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一位置进行赋值导致的。
    • 注意:改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

    2.8.3 并发下诡异的HashMap

    • HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也可能会遇到意想不到的错误。不过和ArrayList不同,HashMap的问题似乎更加诡异。
    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的数字,比如98868。
      • 第三,程序永远无法结束。
    • 对于前两种可能,和ArrayList的情况非常类似。
    • 使用jstack工具显示程序的线程信息,如下所示。其中jps可以显示当前系统中所有的Java进程。而jstack可以打印给定Java进程的内部线程及其堆栈。
    C:Usersgeym >jps
    14240 HashMapMultiThread
    1192 Jps
    C:Usersgeym >jstack 14240
    


    • 可以看到,主线程main正处于等待状态,并且这个等待是由于join()方法引起的,符合我们的预期。而t1和t2两个线程都处于Runnable状态,并且当前执行语句为HashMap.put()方法。
    • 查看put()方法的代码,如下所示:
    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;
        }
    }
    
    • 可以看到,当前两个线程正在遍历HashMap的内部数据。当前所处循环乍看之下是一个迭代遍历,就如同遍历一个链表一样。但在此时此刻,由于多线程的冲突,这个链表的结构已经遭到了破坏,链表成环了!当链表成环时,上述的迭代就等同于一个死循环,如图2.9所示,展示了最简单的一种环状结构,Key1和Key2互为对方的next元素。此时,通过next引用遍历,将形成死循环。

    • 这个死循环的问题,如果一旦发生,着实可以让你郁闷一把。但这个死循环的问题在JDK8中已经不存在了。由于JDK8对HashMap的内部实现了做了大规模的调整,因此规避了这个问题。但即使这样,贸 然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案就是使用ConcurrentHashMap代替HashMap。

    2.8.4 初学者常见问题:错误的加锁

    • 现在,假设我们需要一个计数器,这个计数器会被多个线程同时访问。为了确保数据正确性,我们自然会需要对计数器加锁,因此,就有了以下代码:
    public class BadLockOnInteger implements Runnable {
        public static Integer i = 0;
        static BadLockOnInteger instance = new BadLockOnInteger();
        @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);
        }
    }
    
    • 为了保证计数器i的正确性,每次对i自增前,都先获得i的锁,以此保证i是线程安全的。但我们得到一个比20000000小很多的数。

    • 要解释这个问题,得从Integer说起,在Java中,Integer属于不变对象。也就是对象一旦被创建,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它就永远表示1,你不可能修改Integer的值,使它为2.

    • 使用javap反编译这段代码的run()方法,我们可以看到:

    • 从结果中看出,实际上使用了Integer.valueOf()方法新建了一个新的Integer对象,并将它赋值给变量i。也就是说,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对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。
    • 修正这个问题也容易,只要将
    sychronized(i) {
    
    • 改为:
    synchronized(instance) {
    
  • 相关阅读:
    4-vim-工作模式-01-职责以及切换模式
    3-vim-打开和新建文件-02-删除交换文件
    poj1011Stick(dfs+剪枝)
    POJ 1251 Jungle Roads (prim)
    poj 2502 Subway
    poj 3624 Charm Bracelet (01背包)
    拦截导弹问题(动态规划)
    Policy Gradient
    深入了解马尔科夫决策过程(Markov Decision Process)
    深度学习中调参对模型容量的影响
  • 原文地址:https://www.cnblogs.com/sanjun/p/8319295.html
Copyright © 2011-2022 走看看