前言

Java 中 ArrayList 和 LinkedList 都不是线程安全的,但可以通过 java.util.Collections.synchronizedList(List list) 方法,获取一个线程安全的 List 实例对象。

设计意图

将非线程安全 List 对象,封装成一个线程安全的 List 对象,处理 List 上的并发性问题。类似一个工具类,减少开发人员的重复性工作。

线程安全测试用例

1.定义个 Obj 类,内部使用一个 Collections.synchronizedList(List) 构造一个线程安全的 List 对象。

  1. static class Obj {
  2. private List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
  3. public void add(String s) {
  4. synchronizedList.add(s);
  5. }
  6. public void remove(String s) {
  7. synchronizedList.remove(s);
  8. }
  9. public int size() {
  10. return synchronizedList.size();
  11. }
  12. }

2.创建一个 Obj 对象 obj, 两个线程 t1 和 t2,并在 t1 和 t2 中访问 obj 对象,并发的向 obj 对象中添加元素。每个线程循环执行 100 次添加操作。

  1. public class Test {
  2. public static void main(String[] args) {
  3. final Obj obj = new Obj();
  4. Thread t1 = new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. String val = null;
  8. for (int i = 0; i < 100; i++) {
  9. val = "t1: " + i;
  10. obj.add(String.valueOf(val));
  11. System.out.println("add: " + val);
  12. sleep(50);
  13. }
  14. }
  15. });
  16. Thread t2 = new Thread(new Runnable() {
  17. @Override
  18. public void run() {
  19. String val = null;
  20. for (int i = 0; i < 100; i++) {
  21. val = "t2: " + (100 + i);
  22. obj.add(String.valueOf(val));
  23. System.out.println("add: " + val);
  24. sleep(50);
  25. }
  26. }
  27. });
  28. t1.start();
  29. t2.start();
  30. try {
  31. t1.join(); //等待线程 t1 结束
  32. t2.join(); //等待线程 t2 结束
  33. } catch (Exception e) {
  34. }
  35. String name = Thread.currentThread().getName();
  36. System.out.println("Thread name: " + name + ", Obj size: " + obj.size());
  37. }
  38. static void sleep(long millis) {
  39. try {
  40. Thread.sleep(millis);
  41. } catch (Exception e) {
  42. System.out.println("Exception: " + e.getMessage());
  43. }
  44. }
  45. }

在步骤 2 中,最终的输出结果为,

... ... add: t1: 95 add: t2: 194 add: t2: 195 add: t1: 96 add: t2: 196 add: t1: 97 add: t1: 98 add: t2: 197 add: t2: 198 add: t1: 99 add: t2: 199 Thread name: main, Obj size: 200

可以看到,最终 obj 的 size 为预期的 200

错误用例

如果将 Obj 类中的 List 改为普通的 ArrayList,如下,

  1. static class Obj {
  2. private List<String> list = new ArrayList<>();
  3. public void add(String s) {
  4. list.add(s);
  5. }
  6. public void remove(String s) {
  7. list.remove(s);
  8. }
  9. public int size() {
  10. return list.size();
  11. }
  12. }

使用同样的测试用例,最终的输出结果为,

... ... add: t2: 193 add: t1: 94 add: t2: 194 add: t1: 95 add: t2: 195 add: t1: 96 add: t2: 196 add: t1: 97 add: t2: 197 add: t1: 98 add: t2: 198 add: t1: 99 add: t2: 199 Thread name: main, Obj size: 185

从上面的结果可知,最终 obj 的 size 只有 185,跟我们预期的 200 不一样。表示出现了并发性问题。

原理

正常情况下,Collections.synchronizedList(List list) 返回的是一个 SynchronizedList 的对象,这个对象以组合的方式将对 List 的接口方法操作,委托给传入的 list 对象,并且对所有的接口方法对象加锁,得到并发安全性。
Collections.synchronizedList(List list) 方法源码

  1. public static <T> List<T> synchronizedList(List<T> list) {
  2. return (list instanceof RandomAccess ?
  3. new SynchronizedRandomAccessList<>(list) :
  4. new SynchronizedList<>(list));
  5. }

当传入的 list 是 ArrayList 时,返回 SynchronizedRandomAccessList 对象;传入 LinkedList 时,返回 SynchronizedList 对象。
再来看看 SynchronizedList 源码,

  1. static class SynchronizedList<E>
  2. extends SynchronizedCollection<E>
  3. implements List<E> {
  4. ...
  5. ...
  6. final List<E> list;
  7. SynchronizedList(List<E> list) {
  8. super(list);
  9. this.list = list;
  10. }
  11. SynchronizedList(List<E> list, Object mutex) {
  12. super(list, mutex);
  13. this.list = list;
  14. }
  15. ...
  16. ...
  17. public E get(int index) {
  18. synchronized (mutex) {return list.get(index);}
  19. }
  20. public E set(int index, E element) {
  21. synchronized (mutex) {return list.set(index, element);}
  22. }
  23. public void add(int index, E element) {
  24. synchronized (mutex) {list.add(index, element);}
  25. }
  26. public E remove(int index) {
  27. synchronized (mutex) {return list.remove(index);}
  28. }
  29. ...
  30. ...
  31. }

可以看到,SynchronizedList 的实现里,get, set, add 等操作都加了 mutex 对象锁,再将操作委托给最初传入的 list。
这就是以组合的方式,将非线程安全的对象,封装成线程安全对象,而实际的操作都是在原非线程安全对象上进行,只是在操作前给加了同步锁。
由于有很多业务场景下都有这种需求,所以 Java 类库中封装了这个工具类,给需要的模块使用。