现在多cpu成为常规配置,常常要考虑用多线程来改善程序处理性能。虽然应该尽量使用无副作用的方式来设计并发程序,但终归免不了有的时候需要多线程共享一个或者多个数据,并可能同时修改共享数据的状态。所以,对java的并发处理有进一步的理解可以帮助我进行多线程的设计。
Java 提供了很多并发关键字和 api 类库,我学习孔乙己,用几种方式来完成一个常见的问题:同一时刻只允许几个线程在执行任务。通过对不同api的使用来加深理解。
这里列出了 7 种方式,这些例子主要是为了体验 api 而不是提供设计框架,它们很多地方都不够健壮,各种方式的优缺点及性能差异也不在本文的讨论范围之内。
首先是定义一个信号量接口,并提供一个测试类来测试各种实现方式。
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
// 工作线程需要申请到信号才能执行任务
// 如果信号都发出去了,则阻塞,等到有其他线程释放信号之后再继续申请
// 阻塞可以是先停下来,等待唤醒;这样可以释放 cpu
// 也可以不停的轮询,直到有信号;这样会占住 cpu
interface IMySemaphore {
/*
* if thread be interrupted.
* the thread should not get the permit
* and should not release the semaphore
*/
public void acquire() throws InterruptedException; // 请求信号
public void release(); // 释放信号
}
// 测试运行类
public class SemaphoreTest {
private static void println(final String msg) {
System.out.println(msg);
}
// 每隔一秒打印一个点,好看清楚运行过程
static class TimerPrint implements Runnable {
public void run() {
for (int i = 0; i < 35; i++) {
println(".");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException ie) {
}
}
println("END");
}
}
// 工作类,申请信号 >> 执行 >> 释放信号
static class Worker implements Runnable {
private final int _id; // 工人编号
private final long _wait; // 等一会儿
private final IMySemaphore _semaphore; // 信号量
private final long _lCreateTime;
Worker(final int id, final IMySemaphore semaphore, final long now) {
_id = id;
_wait = (10 - _id) * 1000;
_semaphore = semaphore;
_lCreateTime = now;
}
public void run() {
try {
// 申请信号
_semaphore.acquire();
try {
// 申请到信号,执行对资源的访问等操作,简单的 sleep 一会儿
println(String.format("Worker[%d] is running...", _id));
Thread.currentThread().sleep(_wait);
long period = (System.currentTimeMillis() - _lCreateTime) / 1000;
println(String.format("Worker[%d] is end, %d seconds after created.", _id, period));
} catch (InterruptedException ie) {
// sleep 被中断时,处理中断过程
} finally {
_semaphore.release(); // 释放信号
}
} catch (InterruptedException ie) { // 申请信号时线程被中断
// 通常,中断的使用场景有以下几个:
// 点击某个桌面应用中的取消按钮时;
// 某个操作超过了一定的执行时间限制需要中止时;
// 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
// 一组线程中的一个或多个出现错误导致整组都无法继续时;
// 当一个应用或服务需要停止时。
long period = (System.currentTimeMillis() - _lCreateTime) / 1000;
println(String.format("Worker[%d] is interrupted, %d seconds after created.", _id, period));
return;
}
}
} // END: Worker
// 测试方法
// 运行的时候可以观察到每次只有两个工作线程在执行,
// 每个 worker 执行 10 - id 的秒数,一结束就会唤醒一个等待信号的 worker 继续运行
// 在我机器上,轮询方式大约 28 秒执行完毕;wait/notify 方式大约 31 秒执行完毕
private static void testTimesLock(final IMySemaphore semaphore) {
// 每隔一秒打印一个点
new Thread(new TimerPrint()).start();
// 启动 10 个工作线程,查看信号申请情况
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Worker(i, semaphore, System.currentTimeMillis()));
threads[i].start();
}
try {
Thread.currentThread().sleep(2);
} catch (InterruptedException ie) {
}
// 将第九个中断掉
threads[9].interrupt();
// 等待所有线程结束
for (int i = 0; i < 10; i++) {
try {
threads[i].join();
} catch (InterruptedException ie) {
}
}
}
public static void main(final String[] args) {
// 最多两个线程申请到信号
int max = 2;
testTimesLock(new MySemaphore(max));
// testTimesLock(new SyncSema(max));
// testTimesLock(new LockSema(max));
// testTimesLock(new AtomicSema(max));
// testTimesLock(new AQSSema(max));
// testTimesLock(new LockSupportSema(max));
// 测试重入要用重入的 worker
}
} // END: SemaphoreTest
/**
* ----------------------------------------------------
* 首先是最简单的方式,使用并发包中的Semaphore实现
* ----------------------------------------------------
*/
class MySemaphore implements IMySemaphore {
private final Semaphore _iPermits;
MySemaphore(final int i) {
_iPermits = new Semaphore(i);
}
public void acquire() throws InterruptedException {
_iPermits.acquire();
}
public void release() {
_iPermits.release();
}
}