带你认识线程安全的集合操作

 2022-09-14
原文地址:https://cloud.tencent.com/developer/article/1676952

1、基本认识

简单来讲,我们平时经常使用的ArrayList不是线程安全的。我们通过使用 Collections.synchronizedList 来包装一个线程安全的ArrayList。

我们一般创建一个普通的list是这样的姿势,

    List<String> list = new ArrayList<>();
    
            // populate the list
            list.add("apple");
            list.add("orange");
            list.add("banana");

然后我们把这个list变成线程安全的list,操作的姿势是这样的,

    List<String> syncList = Collections.synchronizedList(list);

这两个list在单线程下使用没有任何不同。

Collections.synchronizedList

2、使用场景

我们来看一个多线程访问list的示例,

    public class SynchronizedListTest {
    
        List<String> list = new ArrayList<>();
        List<String> syncList = Collections.synchronizedList(list);
    
        @Test
        public void test1() {
            list.add("apple");
            list.add("orange");
            list.add("banana");
            System.out.println("sync list:" + syncList);
            Thread t1 = new FruitManager();
            Thread t2 = new FruitManager();
            t1.start();
            t2.start();
        }
    
        class FruitManager extends Thread {
            public void run() {
                System.out.println("thread - " + Thread.currentThread().getName() + " start");
                if (syncList.size() > 0) {
                    String fruit = (String) syncList.remove(0);
                    System.out.println("fruit:" + fruit);
                }
    
            }
        }

很明显,这种场景如果不使用线程安全的集合,就会造成集合中的元素被重复remove的情况,从而导致程序出错。

3、源码剖析

好的程序员不光要知其然,还要知其所以然。如果只是停留在会用阶段,你永远都只是一个初级的程序员。

Collections.synchronizedList 源码也比较简单,就是给所有的操作就加上了 synchroinzed 锁。这里截取部分代码,

      static class SynchronizedList<E>
            extends SynchronizedCollection<E>
            implements List<E> {
            private static final long serialVersionUID = -7754090372962971524L;
    
            final List<E> list;
    
            SynchronizedList(List<E> list) {
                super(list);
                this.list = list;
            }
            SynchronizedList(List<E> list, Object mutex) {
                super(list, mutex);
                this.list = list;
            }
    
            public boolean equals(Object o) {
                if (this == o)
                    return true;
                synchronized (mutex) {return list.equals(o);}
            }
            public int hashCode() {
                synchronized (mutex) {return list.hashCode();}
            }
    
            public E get(int index) {
                synchronized (mutex) {return list.get(index);}
            }
            public E set(int index, E element) {
                synchronized (mutex) {return list.set(index, element);}
            }
            public void add(int index, E element) {
                synchronized (mutex) {list.add(index, element);}
            }
            public E remove(int index) {
                synchronized (mutex) {return list.remove(index);}
            }
    
            public ListIterator<E> listIterator() {
                return list.listIterator(); // Must be manually synched by user
            }
    
            public ListIterator<E> listIterator(int index) {
                return list.listIterator(index); // Must be manually synched by user
            }
        }

这里的 mutex 则是父类 SynchronizedCollection 中的this对象。

细心的读者可能已经看到了,好像迭代器没有加同步锁!是的,也就是说虽然list是安全的,但是list的迭代器并不是线程安全的。

所以官方建议使用的迭代器的时候,要用如下的姿势,

    List list = Collections.synchronizedList(new ArrayList());
          ...
      synchronized (list) {
          Iterator i = list.iterator(); // Must be in synchronized block
          while (i.hasNext())
              foo(i.next());
      }

4、总结

  1. SynchronizedList可以将List转成线程安全的集合,在多线程环境下建议使用。
  2. 使用迭代器遍历时,即使用了 SynchronizedList 也要手动进行同步处理。