Java 并发案例之售票模拟(线程池、数据安全、concurrent包等)

文章目录

        • 实现案例
        • 涉及知识点
        • 知识点简析
        • 案例源码


实现案例

  • 经典案例 - 售票模拟

注:网上有许多类似的源码,但大多数对并发处理的并不好,仅仅是实现了一下多线程而已。其对数据安全、并发效率的考虑并不多,一旦应用将导致各种问题。本案例多处考虑,分析了三种方案,其中第三种在保证了数据安全的前提下,将效率提高至约顺序执行的 N(线程数)倍。若有不周,敬请指出。

涉及知识点

  • ThreadPoolExecutor
  • ReentrantLock
  • CountDownLatch
  • AtomicInteger
  • 用 lambda 表达式 实现 SAM 接口(例:Runnable)

知识点简析

  • 【强制】(阿里巴巴 Java 开发手册)线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool 和 ScheduledThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
    (线程池的使用)
  • JDK中独占锁的实现除了使用关键字 synchronized 外,还可以使用ReentrantLock。虽然在性能上 ReentrantLock 和 synchronized 没有什么区别,但 ReentrantLock 相比 synchronized 而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。(ReentrantLock(重入锁)功能详解和应用演示)
  • 高并发的情况下,i++无法保证原子性,往往会出现问题,所以引入AtomicInteger类。(AtomicInteger深入理解)
  • CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。(Java并发之CountDownLatch、Semaphore和CyclicBarrier)
  • java8新特性之——lambda表达式的使用*(lambda表达式简介)*
  • lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。(解决方案:见代码第 25 行)

案例源码

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Simulated ticketing
 *
 * @author Ning [email protected]
 * @date 2020-02-04 21:20:04
 */
public class ConcurrentDemo {
     
    // Number of windows
    public static final int N_THREADS = 10;
    public static final int N_TICKETS = 100;
    public static final int TIME_CONSUMING = 10;
    public static final boolean PRINT = false;

    public static void main(String[] args) {
     
        timing(ConcurrentDemo::m1);
        timing(ConcurrentDemo::m2);
        timing(ConcurrentDemo::m3);
    }

    private static void m1() {
     
        final var ref = new Object() {
     
            int i = N_TICKETS;
        };
        int x = 0;
        while (true) {
     
            try {
     
                if ((x = ref.i) == 0) break;
                else ref.i--;
                Thread.sleep(TIME_CONSUMING);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            } finally {
     
                if (PRINT)
                    System.out.println(Thread.currentThread().getName()
                            + " " + x);
            }
        }
        System.out.println("m1 -> Current number of tickets: " + ref.i);
        System.out.println("OK!");
    }

    /**
     * Method 2:
     * By using pessimistic lock, the concurrent security of data
     * is guaranteed, but the efficiency is very low, slightly
     * less than that of single thread (due to the switch
     * between threads).
     */
    private static void m2() {
     
        ExecutorService pool = new ThreadPoolExecutor(N_THREADS,
                N_THREADS, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(N_THREADS));
        ReentrantLock lock = new ReentrantLock();
        CountDownLatch cnt = new CountDownLatch(N_THREADS);
        final var ref = new Object() {
     
            int i = N_TICKETS;
        };
        Runnable task = () -> {
     
            int x = 0;
            while (true) {
     
                try {
     
                    lock.lock();
                    if ((x = ref.i) == 0) {
     
                        cnt.countDown();
                        return;
                    } else ref.i--;
                    Thread.sleep(TIME_CONSUMING);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } finally {
     
                    if (PRINT)
                        System.out.println(Thread.currentThread().getName()
                                + " " + x);
                    lock.unlock();
                }
            }
        };

        for (int j = 0; j < N_THREADS; j++) {
     
            pool.execute(task);
        }

        try {
     
            cnt.await();
            pool.shutdown();
            System.out.println("m2 -> Current number of tickets: " + ref.i);
            System.out.println("OK!");
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
    }

    /**
     * Method 3:
     * Adopt AtomicInteger without lock, realized real
     * parallelism and greatly improve efficiency.
     */
    private static void m3() {
     
        ExecutorService pool = new ThreadPoolExecutor(N_THREADS,
                N_THREADS, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(N_THREADS));
        CountDownLatch cnt = new CountDownLatch(N_THREADS);
        AtomicInteger ai = new AtomicInteger(N_TICKETS);
        Runnable task = () -> {
     
            int x = 0;
            while (true) {
     
                try {
     
                    if ((x = ai.updateAndGet(
                            n -> n == 0 ? n : n - 1)) == 0) {
     
                        cnt.countDown();
                        return;
                    }
                    Thread.sleep(TIME_CONSUMING);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } finally {
     
                    if (PRINT)
                        System.out.println(Thread.currentThread().getName()
                                + " " + x);
                }
            }
        };

        for (int j = 0; j < N_THREADS; j++) {
     
            pool.execute(task);
        }

        try {
     
            cnt.await();
            pool.shutdown();
            System.out.println("m3 -> Current number of tickets: " + ai.get());
            System.out.println("OK!");
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
    }

    private static void timing(Runnable action) {
     
        long startTime;
        long endTime;
        startTime = System.currentTimeMillis();
        action.run();
        endTime = System.currentTimeMillis();
        System.out.println("Spent: " + (endTime - startTime) + " ms");
    }
}

你可能感兴趣的:(Java高级,并发编程,java,后端,安全,多线程)