Collections.synchronizedList()方法有什么用?它似乎没有同步列表

时间:2016-12-02 11:10:40

标签: java multithreading collections synchronization

我正在尝试使用两个线程将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()没有进行同步?

6 个答案:

答案 0 :(得分:18)

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

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

例如,假设您的列表中有两个帖子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操作。因此您的输出是完全正常的。

在你的第二段代码(未注释的代码)中,你清楚地说明一个线程在开始循环之前完全锁定了另一个线程的列表。因此,您可以确保您的某个线程在另一个线程可以访问列表之前运行完整循环。

答案 1 :(得分:8)

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

例如,这里是add方法的代码

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

这保证了对支持列表的串行访问,因此如果您的2个线程同时调用add,则一个线程将获取锁,添加其元素并释放锁,然后第二个线程将能够获取锁并添加其元素,这就是您在输出中选择onetwo的原因。

取消注释同步块后,代码就是

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

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

答案 2 :(得分:2)

可观察行为绝对正确 - 您在代码示例中演示的synchronized方法与synchronizedList不同。在第一种情况下,您同步整个for语句,因此只有一个线程将同时执行它。在第二种情况下,您同步收集方法本身 - 这就是synchronizedList所代表的含义。因此,请确保add方法已同步 - 但不是for方法!

答案 3 :(得分:1)

这是一个很酷的小例子,它基于原始示例和公认的答案,以显示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

答案 4 :(得分:1)

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

如果你所做的只是调用单独的方法,比如 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 :(得分:0)

根据先前的答案,您需要同步访问线程synListtOne上的tTwo。在这种情况下,您可以使用监控器模式来提供安全访问-以便进行线程访问。

下面,我将您的代码修改为可以与其他有相同问题的代码共享。在这段代码中,我仅使用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();
}

}