一、ArrayList遍历问题

1.当只有一个线程迭代遍历ArrayList时:边遍历边修改List元素会出现ConcurrenMdifyedException

      正确方法可以采用迭代器遍历迭代器修改元素

2.当多个线程访问ArrayList时(如:一个线程在遍历,一个线程在删除元素):

     想要线程安全的遍历可以采用:

    1)CopyOnWriteArrayList(CopyOnWriteArrayList用于在遍历为主要操作的情况下替代ArrayList)

       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();
}

  2)线程安全的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) {
    ...
}

 

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();
}

二、同步容器与并发容器

1.同步容器

同步容器的同步原理就是在方法上用 synchronized 修饰。那么,这些方法每次只允许一个线程调用执行。

由于被 synchronized 修饰的方法,每次只允许一个线程执行,其他试图访问这个方法的线程只能等待。显然,这种方式比没有使用 synchronized 的容器性能要差。

代表类:Vector、Stack、HashTable

 

2.并发容器

可以并发的进行修改、遍历等等操作

JDK 的 java.util.concurrent 包(即 juc)中提供了几个非常有用的并发容器。

CopyOnWriteArrayList - 线程安全的 ArrayList
CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。
ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
ArrayBlockingQueue - 数组实现的阻塞队列。
LinkedBlockingQueue - 链表实现的阻塞队列。
LinkedBlockingDeque - 双向链表实现的双端阻塞队列。
ConcurrentHashMap
要点
作用:ConcurrentHashMap 是线程安全的 HashMap。
原理:JDK6 与 JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法。