面试1:线程安全的List?

1,808 阅读4分钟

ZJ面试被问到的问题,我们来一个一个问题看

首先第一个问题,ArrayList是线程安全的吗?

答案是不是,我们可以看看ArrayList的源代码

public E set(int index, E element) { 
       rangeCheck(index);        
        E oldValue = elementData(index);        
        elementData[index] = element;        
        return oldValue;    
}

我们在来看看LinkedList是不是线程安全的

可以看到这些涉及到可能会有多线程操作的函数都没有加任何锁的操作,所以ArrayList是线程不安全的。

第二个问题LinkedList是线程安全的吗?

首先我们看看LinkedList 的源代码

public boolean add(E e) {
        linkLast(e);
        return true;
    }

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
    
public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

没有看到锁所以也是线程不安全的

第三个问题: 要线程安全的LIst的话要怎么做呢?

答案很简单:加锁
······可以是sync关键字加在函数上或者是使用ReentrentLock这种可重入锁锁定代码块都可以。
······实际上就有两个类是基于上面的思路实现的线程安全的List他们分别是:synchronizedList 和CopyOnWriteArrayList。

3.1、synchronizedList 是如何实现线程安全的List的?

我们来看synchronizedList的源代码

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 int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }

这个看起来很简单,加锁 的实现就是简单的在函数里面加上了sync关键字

3.2 CopyOnWriteArrayList是如何保证线程安全的

接下来看看CopyOnWriteArrayList的源码
下面这个是部分函数的代码

public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }
public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

可以看到CopyOnWriteArrayList使用了ReentrantLock加了锁,可重入锁,来实现线程安全的。
接下来看这个代码,CopyOnWriteArrayList中你操作的数据不一定是最新的数据,这一点要注意。

private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) findIndex: {
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

第四个问题 这两个线程安全的List性能如何
这个实验来自网络,我自己跑了一下,结果差不多,这里附上源代码

// package 多线程.线程安全的List比较;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;

/**
 * 测试两个线程安全的List读写速度谁比较快
 * 
 */
class  Main{
    

    private static List<String> syncList = Collections.synchronizedList(new ArrayList<String>());
    private static List<String> copyOnWritearraylist =  new CopyOnWriteArrayList<>();
    private static CountDownLatch cdl1 = new CountDownLatch(2);
    private static CountDownLatch cdl2 = new CountDownLatch(2);
    private static CountDownLatch cdl3 = new CountDownLatch(2);
    private static CountDownLatch cdl4 = new CountDownLatch(2);

    // syncList 写线程
    static class ArrayAddThread extends Thread{
        @Override
        public void run() {
            
            for(int i=0;i<50000;i++){
                syncList.add(String.valueOf(i));
            }
            cdl1.countDown();
        }
    }

    // sync 读线程
    static class  ArrayGetThread extends Thread{
        @Override
        public void run() {
            
            int count  = syncList.size();
            for(int i=0;i<count;i++){
                syncList.get(i);
            }
            cdl2.countDown();
        }
    }
    // copyOnWriteArrayList 写线程
    static class CopyAddThread extends Thread{
        @Override
        public void run() {
            
            for(int i=0;i<50000;i++){
                copyOnWritearraylist.add(String.valueOf(i));
            }
            cdl3.countDown();
        }
    }
    // copyOnWriteArrayList 读线程
    static class CopyGetThread extends Thread{
        @Override
        public void run() {
            int count = copyOnWritearraylist.size();
            for(int i=0;i<count;i++){
                copyOnWritearraylist.get(i);
            }
            cdl4.countDown();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long start1 = System.currentTimeMillis();
        new ArrayAddThread().start();
        new ArrayAddThread().start();
        cdl1.await();//等待countDownLuanch 减到0
        long end1  = System.currentTimeMillis();
        System.out.println("ArrayList 写操作时间: "+(end1-start1));


        long start2 = System.currentTimeMillis();
        new CopyAddThread().start();
        new CopyAddThread().start();
        cdl3.await();//等待countDownLuanch 减到0
        long end2  = System.currentTimeMillis();
        System.out.println("CopyOnWriteArrayList 写操作时间: "+(end2-start2));

        long start3 = System.currentTimeMillis();
        new ArrayGetThread().start();
        new ArrayGetThread().start();
        cdl2.await();//等待countDownLuanch 减到0
        long end3  = System.currentTimeMillis();
        System.out.println("ArrayList 读操作时间: "+(end3-start3));

        long start4 = System.currentTimeMillis();
        new CopyGetThread().start();
        new CopyGetThread().start();
        cdl4.await();//等待countDownLuanch 减到0
        long end4  = System.currentTimeMillis();
        System.out.println("CopyOnWriteArrayList 读操作时间: "+(end4-start4));

    }
    
}

结果

SyncList 写操作时间: 22 CopyOnWriteArrayList 写操作时间: 4443 SyncList 读操作时间: 20 CopyOnWriteArrayList 读操作时间: 9

我们可以得出结论就是,

  • SyncList的读写时间差不多,因为都加了锁
  • CopyOnWriteArrayList的读快很多,但是写慢很多,可以应用与读多写少的场景
  • CopyOnWriteArrayList读出来的可能不是最新的数据,如果对数据要求很严格的话,最好使用SyncList