实现案例
- 经典案例 - 售票模拟
注:网上有许多类似的源码,但大多数对并发处理的并不好,仅仅是实现了一下多线程而已。其对数据安全、并发效率的考虑并不多,一旦应用将导致各种问题。本案例多处考虑,分析了三种方案,其中第三种在保证了数据安全的前提下,将效率提高至约顺序执行的 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 242741154@qq.com
* @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");
}
}