Java多线程学习(五)
集合类是我们常用的开发工具,它们所封装的数据结构给我们的开发带来了极大的便利,随着从单线程开发环境到多线程开发环境的转换,有些集合类的使用不当,会造成不同程度的错误,给程序带来极大的影响。现在就集合类的线程安全性来做一些总结。
1.ArrayList线程不安全
1.1先来看看单线程环境下,ArrayList的add方法和遍历
public class ListTest { public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); for (String s : arrayList) { System.out.println(s); } } }
程序的运行结果很好理解,这里不做过多的解释。
1.2 现在来看看有30个线程对同一个ArrayList进行add和读取的情况:
public class ListTest { public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一个长为5的随机字符串 System.out.println(arrayList); //读取list },"线程"+(i+1)).start(); } } }
由以下运行结果可以看出,多线程的情况下,程序已经出现了异常
java.util.ConcurrentModificationException
抛出异常的原因很明显,就是add方法没有加锁,导致多个线程抢占资源,自然就会抛异常了,那么如何解决它呢?
1.3使用Vector类来解决多线程对数组列表操作引发的
ConcurrentModificationException
public static void main(String[] args) { List<String> list = new Vector<String>();//new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一个长为5的随机字符串 System.out.println(list); //读取list },"线程"+(i+1)).start(); } }
这样子问题就解决了,翻看Vector的源码可知,其add方法使用了synchronized关键字进行加锁,只允许一个线程进来。Vector能够保持数据的一致性,但是访问数据的效率却下降了。下面来看看另外一种解决方案。
1.4使用Collections工具类把ArraryList转为线程安全的List
public static void main(String[] args) { List<String> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 30; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一个长为5的随机字符串 System.out.println(list); //读取list },"线程"+(i+1)).start(); } }
1.5使用java.util.Concurrent下的CopyOnWriteArrayList类代替ArrayList
public static void main(String[] args) { List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一个长为5的随机字符串 System.out.println(list); //读取list },"线程"+(i+1)).start(); } }
CopyOnWriteArrayList是怎么做到的呢?翻看其源码可以知道,它是利用了一种读写分离的思想。
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ 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(); } }
当往一个线程往CopyOnWriteArrayList添加元素时,先加上ReentrantLock,不是直接往Object[ ]添加,而是先将当前容器的Object[ ]先赋值一份,然后使用Array.copy()将获取新的数组,该数组为当前数组的长度+1,用于往该数组中添加元素,往newElements中添加完元素后,把容器的数组setArray(newElements)进行修改,然后将锁释放,给其他的线程来对容器进行修改。
这样做的好处是,可以对CopyOnWriteArrayList容器进行并发读,而不需要加额外的锁,保证了访问的高效性。
2.HashSet线程不安全
照葫芦画瓢,既然HashSet线程不安全,那么解决的方案就有:
- Collections.synchronizedSet(new HashSet<>());
- new CopyOnWriteArraySet<>();
为什么HashSet线程也不安全呢?因为它的底层数据结构是HashMap!(尽量不要说成是数组+链表)HashSet方法的add方法,其实就是调用了HashMap的put方法,add方法要添加的元素作为了HashMap的key被添加进HashSet中,而value是一个Object对象常量。HashMap的put方法线程不安全,那么HashSet方法也显然是线程不安全的。
3.HashMap线程不安全
同样地,解决HashMap线程不安全的方法之一有:
Collections.synchronizedMap(new HashMap<String,String>());
还有一个在java.util.Concurrent包下的
new ConcurrentHashMap<String,String>();
后期,关于分析HashMap源码的博客我也会整理好发布!