Java高级技术第五章——高并发之同步容器

前言

前言点击此处查看:
http://blog.csdn.net/wang7807564/article/details/79113195

同步容器

问题引出:

有N张火车票,每张票都有一个编号,同时有10个窗口对外售票,写一个模拟程序。

public class TicketSeller {

        static List<String> tickets = new ArrayList<>();

        static {

                for(int i=0; i<10000; i++) tickets.add("票编号:" + i);

        }

        public static void main(String[] args) {

                for(int i=0; i<10; i++) {

                        new Thread(()->{

                                while(tickets.size() > 0) {

                                        System.out.println("销售了--" + tickets.remove(0));

                                }

                        }).start();

                }

        }

}

在上述程序运行中,会报错,数组越界:

java.lang.ArrayIndexOutOfBoundsException

很显然,这是因为多线程之间没有处理好同步问题造成的。

Vector

尝试将tickets换为vector类型:

static Vector<String> tickets = new Vector<>();

List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList.
Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。它的内部是使用synchronized关键字来实现同步锁的。
虽然在笔者测试中没有没有出现过数组越界的问题,但是使用Vector是具有潜在的线程不安全风险的。原因是vector对每个读取和写入的方法是单独加锁的,也就是说使用synchronized关键字修饰了读取和写入的方法。但是在上面的例子中,读取和写入应该作为一个整体,这个整体应该保证原子性,单独的读取和写入虽然加锁,但是这个整体仍然是线程不安全的,这个整体对应的就是while循环的代码块。如果想要实现绝对的线程安全,可以改成如下例子,但是是牺牲性能的:

            new Thread(()-> {
                synchronized (new Object()) {
                    {
                        while (tickets.size() > 0) {
                            System.out.println("销售了--" + tickets.remove(0));
                        }
                    }
                }
            }).start();

Collections.synchronizedXXX():

Collections类是一个工厂类,通过这个工厂类来获取一些同步容器的实例。
这些通过工厂方法获取到的实例,内部也是通过synchronized关键字来完成同步的。
Synchronizedxxx方法有两个不足:
1. 首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问hash表。
2. 同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然需要额外的同步。

使用下面的代码,也能达到解决该问题的目的:

static List<String> tickets = Collections.synchronizedList(new ArrayList<>());

在获取tickects实例的时候,使用collections.synchronizedxxx()函数进行同步,这样可以获取一个同步容器的实例。避免了数组越界问题。
对于上述问题,如果使用synchronized关键字也可以实现:

While(true)
Synchronized(tickets)
{
判断size
Remove()移除
}

这里面相当于将size()和remove()两个原子操作合并为一个原子操作,实现了读写锁。但是,这种方法相对来说,更占用CPU资源。

虽然诸如get()和put()之类的简单操作可以在不需要额外同步的情况下安全地完成.但还是有一些公用的操作序列,例如迭代操作或者put-if-absent(空则放入),需要外部的同步,以避免数据争用。
synchronizedMap、synchronizedList等也被称为有条件的线程安全同步的集合包装器:
  也就是说所有单个的操作都是线程安全的.但是多个操作组成的操作序列却可能导致数据争用,因为在操作序列中控制流取决于前面操作的结果。
  所以,在JDK 5版本以后,对于高并发场景推荐使用ConcurrentHashMap.其特点:效率比Hashtable高,并发性比hashmap好,HashMap中未进行同步考虑,而Hashtable则使用了synchronized,带来的直接影响就是可选择,我们可以在单线程时使用HashMap提高效率,而多线程时用Hashtable来保证安全,而ConcurrentHashMap则结合了两者的特点。

猜你喜欢

转载自blog.csdn.net/wang7807564/article/details/80048554