• 怎样线程安全地遍历List:Vector、CopyOnWriteArrayList


    遍历List的多种方式

    在讲怎样线程安全地遍历List之前,先看看通常我们遍历一个List会採用哪些方式。

    方式一:

    for(int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }

    方式二:

    Iterator iterator = list.iterator();
    while(iterator.hasNext()) {
        System.out.println(iterator.next());
    }

    方式三:

    for(Object item : list) {
        System.out.println(item);
    }

    方式四(Java 8):

    list.forEach(new Consumer<Object>() {
        @Override
        public void accept(Object item) {
            System.out.println(item);
        }
    });

    方式五(Java 8 Lambda):

    list.forEach(item -> {
        System.out.println(item);
    });

    方式一的遍历方法对于RandomAccess接口的实现类(比如ArrayList)来说是一种性能非常好的遍历方式。

    可是对于LinkedList这种基于链表实现的List,通过list.get(i)获取元素的性能差。

    方式二和方式三两种方式的本质是一样的,都是通过Iterator迭代器来实现的遍历,方式三是增强版的for循环,能够看作是方式二的简化形式。

    方式四和方式五本质也是一样的,都是使用Java 8新增的forEach方法来遍历。方式五是方式四的一种简化形式,使用了Lambda表达式。

    遍历List的同一时候操作List会发生什么?

    先用非线程安全的ArrayList做个试验,用一个线程遍历List。遍历的同一时候还有一个线程删除List中的一个元素。代码例如以下:

    public static void main(String[] args) {
    
        // 初始化一个list,放入5个元素
        final List<Integer> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            list.add(i);
        }
    
        // 线程一:通过Iterator遍历List
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int item : list) {
                    System.out.println("遍历元素:" + item);
                    // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    
        // 线程二:remove一个元素
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 因为程序跑的太快。这里sleep了1秒来调慢程序的执行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                list.remove(4);
                System.out.println("list.remove(4)");
            }
        }).start();
    }

    执行结果:
    遍历元素:0
    遍历元素:1
    list.remove(4)
    Exception in thread “Thread-0” java.util.ConcurrentModificationException

    线程一在遍历到第二个元素时,线程二删除了一个元素。此时程序出现异常:ConcurrentModificationException。

    试想假设一个老师正在点整个班级所有学生的人数(线程一遍历List),而校长(线程二)同一时候叫走几个学生。那么老师也肯定点不下去了。

    所以我们会想到一个解决方式,那就是校长等待老师点完学生后,再叫走学生。

    即让线程二等待线程一的遍历完毕后再进行remove元素。

    使用线程安全的Vector

    ArrayList是非线程安全的。Vector是线程安全的,那么把ArrayList换成Vector是不是就能够线程安全地遍历了?

    将程序中的:

    final List<Integer> list = new ArrayList<>();

    改成:

    final List<Integer> list = new Vector<>();

    再执行一次试试,会发现结果和ArrayList一样会抛出ConcurrentModificationException异常。

    为什么线程安全的Vector也不能线程安全地遍历呢?事实上道理也非常easy,看Vector源代码能够发现它的非常多方法都加上了synchronized来进行线程同步,比如add()、remove()、set()、get(),可是Vector内部的synchronized方法无法控制到遍历操作。所以即使是线程安全的Vector也无法做到线程安全地遍历。

    假设想要线程安全地遍历Vector,须要我们去手动在遍历时给Vector加上synchronized锁,防止遍历的同一时候进行remove操作。相当于校长等待老师点完学生后,再叫走学生。代码例如以下:

    public static void main(String[] args) {
    
        // 初始化一个list。放入5个元素
        final List<Integer> list = new Vector<>();
        for(int i = 0; i < 5; i++) {
            list.add(i);
        }
    
        // 线程一:通过Iterator遍历List
        new Thread(new Runnable() {
            @Override
            public void run() {
                // synchronized来锁住list。remove操作会在遍历完毕释放锁后进行
                synchronized (list) {
                    for(int item : list) {
                        System.out.println("遍历元素:" + item);
                        // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    
        // 线程二:remove一个元素
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                list.remove(4);
                System.out.println("list.remove(4)");
            }
        }).start();
    }

    执行结果:
    遍历元素:0
    遍历元素:1
    遍历元素:2
    遍历元素:3
    遍历元素:4
    list.remove(4)

    执行结果显示list.remove(4)的操作是等待遍历完毕后再进行的。

    CopyOnWriteArrayList

    CopyOnWriteArrayList是java.util.concurrent包中的一个List的实现类。CopyOnWrite的意思是在写时拷贝。也就是假设须要对CopyOnWriteArrayList的内容进行改变。首先会拷贝一份新的List而且在新的List上进行改动,最后将原List的引用指向新的List。

    使用CopyOnWriteArrayList能够线程安全地遍历,因为假设另外一个线程在遍历的时候改动List的话,实际上会拷贝出一个新的List上改动。而不影响当前正在被遍历的List。

    相当于校长要想从班级喊走或者加入学生。须要把学生所有带到一个新的教室再进行操作,而老师则通过之前班级的快照在照片上清点学生。

    public static void main(String[] args) {
    
        // 初始化一个list,放入5个元素
        final List<Integer> list = new CopyOnWriteArrayList<>();
        for(int i = 0; i < 5; i++) {
            list.add(i);
        }
    
        // 线程一:通过Iterator遍历List
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int item : list) {
                    System.out.println("遍历元素:" + item);
                    // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    
        // 线程二:remove一个元素
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                list.remove(4);
                System.out.println("list.remove(4)");
            }
        }).start();
    }

    执行结果:
    遍历元素:0
    遍历元素:1
    list.remove(4)
    遍历元素:2
    遍历元素:3
    遍历元素:4

    从上面的执行结果能够看出,尽管list.remove(4)已经移除了一个元素,可是遍历的结果还是存在这个元素。由此能够看出被遍历的和remove的是两个不同的List。

    线程安全的List.forEach

    List.forEach方法是Java 8新增的一个方法,主要目的还是用于让List来支持Java 8的新特性:Lambda表达式。

    因为forEach方法是List的一个方法,所以不同于在List外遍历List。forEach方法相当于List自身遍历的方法。所以它能够自由控制是否线程安全。

    我们看线程安全的Vector的forEach方法源代码:

    public synchronized void forEach(Consumer<? super E> action) {
        ...
    }

    能够看到Vector的forEach方法上加了synchronized来控制线程安全的遍历,也就是Vector的forEach方法能够线程安全地遍历

    以下能够測试一下:

    public static void main(String[] args) {
    
        // 初始化一个list,放入5个元素
        final List<Integer> list = new Vector<>();
        for(int i = 0; i < 5; i++) {
            list.add(i);
        }
    
        // 线程一:通过Iterator遍历List
        new Thread(new Runnable() {
            @Override
            public void run() {
                list.forEach(item -> {
                    System.out.println("遍历元素:" + item);
                    // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        }).start();
    
        // 线程二:remove一个元素
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 因为程序跑的太快,这里sleep了1秒来调慢程序的执行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                list.remove(4);
                System.out.println("list.remove(4)");
            }
        }).start();
    }

    执行结果:
    遍历元素:0
    遍历元素:1
    遍历元素:2
    遍历元素:3
    遍历元素:4
    list.remove(4)

    转载请注明原文地址:http://xxgblog.com/2016/04/02/traverse-list-thread-safe/

  • 相关阅读:
    linux的lsof命令
    linux find
    linux 查看磁盘空间大小
    eclipse运行时编码设置
    WebService工作原理
    Java获取字符串编码方式
    JavaScript,base64加密解密
    如何用javascript 的eval动态执行一个需要传对象参数的函数
    struts2结果类型
    执行maven-build.cmd失败
  • 原文地址:https://www.cnblogs.com/yxysuanfa/p/7295618.html
Copyright © 2020-2023  润新知