【问题标题】:What is the use of Collections.synchronizedList() method? It doesn't seem to synchronize the listCollections.synchronizedList() 方法有什么用?它似乎没有同步列表
【发布时间】:2016-12-02 11:10:40
【问题描述】:

我正在尝试使用两个线程将 String 值添加到 ArrayList。我想要的是,当一个线程添加值时,另一个线程不应该干扰,所以我使用了Collections.synchronizedList 方法。但似乎如果我没有在对象上显式同步,则添加是以非同步方式完成的。

没有显式同步块:

public class SynTest {
    public static void main(String []args){
        final List<String> list=new ArrayList<String>();
        final List<String> synList=Collections.synchronizedList(list);
        final Object o=new Object();
        Thread tOne=new Thread(new Runnable(){

            @Override
            public void run() {
                //synchronized(o){
                for(int i=0;i<100;i++){
                    System.out.println(synList.add("add one"+i)+ " one");
                }
                //}
            }

        });

        Thread tTwo=new Thread(new Runnable(){

            @Override
            public void run() {
                //synchronized(o){
                for(int i=0;i<100;i++){
                    System.out.println(synList.add("add two"+i)+" two");
                }
                //}
            }

        });
        tOne.start();
        tTwo.start();
    }
}

我得到的输出是:

true one
true two
true one
true two
true one
true two
true two
true one
true one
true one...

在未注释显式同步块的情况下,我将在添加时停止来自其他线程的干扰。一旦线程获得了锁,它就会一直执行直到完成。

取消注释同步块后的示例输出:

true one
true one
true one
true one
true one
true one
true one
true one...

那么为什么Collections.synchronizedList() 不进行同步呢?

【问题讨论】:

标签: java multithreading collections synchronization


【解决方案1】:

同步列表仅同步该列表的方法。

这意味着当另一个线程当前正在运行此列表中的方法时,一个线程将无法修改该列表。处理方法时对象被锁定。

例如,假设有两个线程在您的列表上运行 addAll,并使用 2 个不同的列表(A=A1,A2,A3B=B1,B2,B3)作为参数。

  • 由于方法是同步的,您可以确保这些列表不会像A1,B1,A2,A3,B2,B3那样随机合并

  • 您无法决定一个线程何时将进程移交给另一个线程。每个方法调用都必须完全运行并返回,然后才能运行另一个。所以你可以得到A1,A2,A3,B1,B2,B3B1,B2,B3,A1,A2,A3(因为我们不知道哪个线程调用会先运行)。

在您的第一段代码中,两个线程同时运行。两者都尝试将add 一个元素添加到列表中。除了 add 方法上的同步之外,您没有任何方法可以阻止一个线程,因此在将进程移交给线程 2 之前,没有什么可以阻止线程 1 运行多个 add 操作。所以您的输出是完全正常的。

在您的第二段代码(未注释的代码)中,您明确声明一个线程在开始循环之前将列表与另一个线程完全锁定。因此,您要确保其中一个线程在另一个线程可以访问列表之前运行完整的循环。

【讨论】:

    【解决方案2】:

    Collections.synchronizedList() 将同步对支持列表的所有访问,但迭代时仍需要在同步块内完成,同步的 List 实例作为对象的监视器。

    例如这里是add方法的代码

    public boolean add(E e) {
        synchronized (mutex) {return c.add(e);}
    }
    

    这保证了对backed list的串行访问,所以如果你的2个线程同时调用add,一个线程将获取锁,添加它的元素并释放锁,然后第二个线程将能够获取锁定并添加它的元素,这就是为什么你会在输出中交替得到onetwo

    当你取消同步块的注释时,代码就是

    synchronized(o) {
        for(int i=0;i<100;i++){
            ...
        }
    }
    

    在这种情况下,首先可以获取o 锁的线程将在释放锁之前执行整个 for 循环(除非抛出异常),从而允许其他线程执行其同步块的内容,这就是为什么你会得到100 连续多次onetwo 然后100 连续多次其他值。

    【讨论】:

      【解决方案3】:

      这是一个基于原始示例和公认答案的很酷的小示例,以显示 synchronizedList 的用途。

      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.Collections;
      import java.util.List;
      
      public class SynTest {
          public static void main(String []args) throws InterruptedException
          {
              final List<String> list = new ArrayList<>();
              final List<String> synList = Collections.synchronizedList(new ArrayList<>());
      
              Thread t1 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      list.addAll(Arrays.asList("one", "one", "one"));
                      synList.addAll(Arrays.asList("one", "one", "one"));
                  }
              });
      
              Thread t2 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      list.addAll(Arrays.asList("two", "two", "two"));
                      synList.addAll(Arrays.asList("two", "two", "two"));
                  }
              });
      
              t1.start();
              t2.start();
      
              Thread.sleep(1000);
              System.out.println(list);
              System.out.println(synList);
          }
      }
      

      原来的list 最终会出现未定义的行为,结果如下:

      [one, one, one] // wrong!
      [one, one, one, null, null, null] // wrong!
      [two, two, two] // wrong!
      [one, one, one, two, two, two] // correct
      

      虽然同步的synList 有一个同步的addAll 方法并且总是产生两个正确结果之一:

      [one, one, one, two, two, two] // correct
      [two, two, two, one, one, one] // correct
      

      【讨论】:

      • 我很困惑为什么原始列表的结果中会有一些空对象。这里有什么问题?
      • 我认为我们还应该在这里提到使用addAll 而不是addaddAll 方法是一种批量操作,与 add 不同,这就是 @Mo 没有遇到与原始操作相同的问题的原因。这也是他不需要使用synchronize 关键字的原因,因为Collections.synchronizedList 会为每个操作释放锁。在这种情况下,锁定在addAll 操作后被释放。
      【解决方案4】:

      有一个警告,所有发布的答案都错过了。就是这样:Collections.synchronizedList 将返回 List 类型数据结构的包装“线程安全”版本,但它不会同步列表上的操作。您仍然需要同步支持数据结构上的操作以使其真正多线程安全。

      如果您所做的只是调用单个方法,例如 add()、remove()、size() 等。您仍然可以得到竞争条件,因为您不知道这些操作将以什么顺序执行,除非您同步它们。示例

      synchronize(list){ 
       // ^ without this line the code below is not really thread-safe
      
              while( i++ <list.size() )
                  if (testCondition( list.get() ) ) 
                        list.remove();                 
      
      }
      

      【讨论】:

        【解决方案5】:

        可观察到的行为绝对正确 - 您在代码示例中演示的 synchronized 方法与 synchronizedList 方法的作用不同。

        在第一种情况下,您同步整个for-statement,因此只有一个线程会同时执行它。 在第二种情况下,您自己同步收集方法——这就是synchronizedList 的含义。所以 add 方法是同步的 - 但不是 for 方法!

        【讨论】:

          【解决方案6】:

          根据之前的答案,您需要从访问线程tOnetTwo 同步synList。在这种情况下,您可以使用监视器模式来提供安全访问 - 为了线程。

          在下面,我调整了您的代码,以便与遇到相同问题的其他人共享。在这段代码中,我只使用了synList 以同步方式控制访问。请注意,不必创建其他对象来确保从 synList 进行订单访问。 为了补充这个问题,请参阅 Java Concurrency in Practice jcip 第 4 章一书,其中讨论了受 Hoare 工作启发的监视器设计模式

          public class SynTest {
          public static void main(String []args){
              final List<String> synList= Collections.synchronizedList(new ArrayList<>());
          
              Thread tOne=new Thread(() -> {
                  synchronized (synList) {
                      for (int i = 0; i < 100; i++) {
                          System.out.println(synList.add("add one" + i) + " one");
                          try {
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              });
          
              Thread tTwo=new Thread(()->{
                  synchronized (synList) {
                      for(int i=0;i<100;i++){
                          System.out.println(synList.add("add two"+i)+" two");
                          try {
                              Thread.sleep(200);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      }
          
              });
              tOne.start();
              tTwo.start();
          }
          

          }

          【讨论】:

            猜你喜欢
            • 2013-08-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-04-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多