如何同步Java ArrayList

时间:2020-01-09 10:35:01  来源:igfitidea点击:

这篇文章展示了如何在Java中同步ArrayList以及其他可用的线程安全替代方法。

Java中的ArrayList不是线程安全的,因为默认情况下它不同步。如果在多线程环境中使用ArrayList,并且同时由多个线程访问它,甚至在单个线程中也对其进行结构修改,则必须在外部进行同步。结构修改定义为添加或者删除一个或者多个元素,或者显式调整后备数组大小的任何操作;仅设置元素的值不是结构上的修改。

线程安全列表的选项

如果要在Java中同步ArrayList或者寻找ArrayList的线程安全替代方法,则有以下选项。

  • 使用Vector类– Vector已同步并且List的线程安全实现。但是问题在于,所有方法都在单个锁上同步,因此在任何时候,即使是get()方法,也只有一个线程可以使用Vector。这使得Vector使用起来非常缓慢。

  • 使用Collections.synchronizedList()方法–使用此方法可以同步ArrayList。

  • 使用CopyOnWriteArrayList –另一个选择是使用CopyOnWriteArrayList,它是ArrayList的线程安全变体。在CopyOnWriteArrayList中,所有可变操作(添加,设置)都是通过对基础数组进行全新复制来实现的。由于每次更改List都会创建一个新副本,因此使用CopyOnWriteArrayList通常太昂贵。如果遍历操作多于变异,并且我们不想同步遍历,则效率可能更高。

使用Collections.synchronizedList()方法

在看到使用Collections.synchronizedList()方法在Java中同步ArrayList的示例之前,让我们看一下如果在多线程环境中使用ArrayList而不进行同步会发生什么情况。

在Java程序中,创建了四个线程,每个线程将5个元素添加到列表中。完成所有线程后,列表大小应为20。

import java.util.ArrayList;
import java.util.List;

public class ListSynchro implements Runnable{    
  private List<Integer> normalList;    
  public ListSynchro(List<Integer> normalList){
    this.normalList = normalList;
  }
    
  public static void main(String[] args) {
    List<Integer> normalList = new ArrayList<Integer>();
    Thread t1 = new Thread(new ListSynchro(normalList));
    Thread t2 = new Thread(new ListSynchro(normalList));
    Thread t3 = new Thread(new ListSynchro(normalList));
    Thread t4 = new Thread(new ListSynchro(normalList));
        
    t1.start();
    t2.start();
    t3.start();
    t4.start();
        
    try {
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    } catch (InterruptedException e) {    
      e.printStackTrace();
    }
    System.out.println("Size of list is " + normalList.size());
  }

  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    for(int i = 0; i < 5; i++){
      normalList.add(i);
      try {
        // delay to verify thread interference
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

in run methodThread-0
in run methodThread-2
in run methodThread-3
Size of list is 15

如我们所见,由于线程干扰,列表的运行大小之一为15.

这又是同一示例,其中ArrayList被同步以使其线程安全。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ListSynchro implements Runnable{    
  private List<Integer> normalList;   
  public ListSynchro(List<Integer> normalList){
    this.normalList = normalList;
  }
    
  public static void main(String[] args) {
    // Synchronized ArrayList
    List<Integer> normalList = Collections.synchronizedList(new ArrayList<Integer>());
    Thread t1 = new Thread(new ListSynchro(normalList));
    Thread t2 = new Thread(new ListSynchro(normalList));
    Thread t3 = new Thread(new ListSynchro(normalList));
    Thread t4 = new Thread(new ListSynchro(normalList));
        
    t1.start();
    t2.start();
    t3.start();
    t4.start();
        
    try {
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    } catch (InterruptedException e) {    
      e.printStackTrace();
    }
    System.out.println("Size of list is " + normalList.size());

  }

  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    for(int i = 0; i < 5; i++){
      normalList.add(i);
      try {
        // delay to verify thread interference
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

in run methodThread-1
in run methodThread-0
in run methodThread-3
in run methodThread-2
Size of list is 20

迭代同步列表

根据Java文档,即使我们使用Collections.synchronizedList()获取同步列表,也必须在通过Iterator,Spliterator或者Stream遍历返回列表时手动对其进行同步:

public class ListItr {
  public static void main(String[] args) {
    List<String> carList = Collections.synchronizedList(new ArrayList<String>());
    carList.add("Audi");
    carList.add("Jaguar");
    carList.add("BMW");
    carList.add("Mini Cooper");
    synchronized (carList) {
      // Must be in synchronized block
      Iterator<String> itr = carList.iterator(); 
      while (itr.hasNext()) {
        System.out.println(itr.next());
      }
    }
  }
}

使用CopyOnWriteArrayList

具有线程安全列表的另一种选择是使用CopyOnWriteArrayList。由于如果有任何突变,将创建该列表的新副本,因此不会造成线程干扰。
让我们用一个简单的示例来查看它,在该示例中创建一个CopyOnWriteArrayList,然后对其进行迭代。在迭代过程中,使用List的remove方法删除了一个元素,但仍然不会抛出ConcurrentModificationException。在输出中,我们可以看到迭代仍在显示所有元素,因为在CopyOnWriteArrayList的单独副本上进行了迭代。

import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyItr {
  public static void main(String[] args) {
    List<String> carList = new CopyOnWriteArrayList<String>();
    carList.add("Audi");
    carList.add("Jaguar");
    carList.add("BMW");
    carList.add("Mini Cooper");
    Iterator<String> i = carList.iterator(); 
    while (i.hasNext()){            
      carList.remove("Jaguar");
      System.out.println(i.next()); 
    } 
    System.out.println("List after removal" + carList); 
  }
}

输出:

Audi
Jaguar
BMW
Mini Cooper
List after removal[Audi, BMW, Mini Cooper]