引言

现有一个需求如下:

有10000张火车票,每张票都有一个编号,同时有10个窗口对外售票,如何确保车票的正常售卖?

程序一:使用List

问题的解决办法都是从我们最最熟悉的角度思考。程序一,我们使用一个普通的List作为方案。

阅读以下代码,观察执行结果:

public class TicketSell_01 {
    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();
        }
    }
}

输出结果如下,可以看到,编号0的车票被销售了两次。List不是同步容器,容器内的remove()等方法都无法做到原子性,因此会出现重复售票的问题,因此是不安全的:

Java并发编程实战————售票问题-LMLPHP

程序二:使用Vector

以Vector代替List作为容器。区别是,Vector是同步容器,内部的方法都是同步的。但是下面的代码依然会存在问题。

虽然size()方法和remove()方法本身是原子性的,其他线程无法打断,但是在判断size和remove之间的部分依然会有线程交叉执行的可能,这样,虽然可以解决重复销售的问题,但是依然会导致:ArrayIndexOutOfBoundsException

public class TicketSell_02 {
    static Vector<String> tickets = new Vector<>();

    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) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(5);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("销售了--" + tickets.remove(0));
                }
            }).start();
        }
    }
}

执行结果:

Java并发编程实战————售票问题-LMLPHP

程序三:同步

使用synchronized进行线程同步,可以有效解决逻辑问题,但是很明显,这种方法的缺点就是效率低下。

public class TicketSell_03 {
    static List<String> tickets = new LinkedList<>();

    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 (true) {
                    synchronized (tickets) {
                        if (tickets.size() == 0)
                            break;
                        System.out.println("销售了票--" + tickets.remove(0));
                    }
                }
            }).start();
        }
    }
}

程序四:并发容器Queue

ConcurrentLinkedQueue是一个并发队列但凡并发容器,其内部的方法都保证是原子性的。下面的代码中poll()表示从队列的头部获得一个数据,当返回值为null时,代表这个队列已经没有值了。因为队列本身不允许存null值,否则会报空指针异常,因此当返回值为null时,一定表示队列已空(size() == 0)。队列的底层是使用一个叫做CompareAndSet(CAS)的技术实现的,不是加锁的实现,因此在高并发的情况下依然可以拥有很高的效率。

public class TicketSell_04 {
    static Queue<String> tickets = new ConcurrentLinkedQueue<>();

    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 (true) {
                    String s = tickets.poll();
                    if (s == null)
                        break;
                    else
                        System.out.println("销售了--" + s);
                }
            }).start();
        }
    }
}

 

10-10 04:27