Java并发工具包详解
针对并发编程,Java提供了很多并发工具类供我们使用,下面我们详细介绍一下。
Semaphore

- init():设置计数器的初始值。
- down():计数器的值减1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行。
- up():计数器的值加1;如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
如何使用信号量
static int count;
//初始化信号量
static final Semaphore s
= new Semaphore( 1 );
//用信号量保证互斥
static void addOne() {
s.acquire();
try {
count+= 1 ;
} finally {
s.release();
}
}
下面再来分析一下,信号量是如何保证互斥的。假设两个线程T1和T2同时访问addOne()方法,当它们同时调用acquire()的时候,由于acquire()是一个原子操作,所以只能有一个线程(假设T1)把信号量里的计数器减为0,另外一个线程(T2)则是将计数器减为-1。对于线程T1,信号量里面的计数器的值是0,大于等于0,所以线程T1会继续执行;对于线程T2,信号量里面的计数器的值是-1,小于0,按照信号量模型里对down()操作的描述,线程T2将被阻塞。所以此时只有线程T1会进入临界区执行count+=1;。
快速实现一个限流器
class ObjPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for(int i=0; i<size; i++){
pool.add(t);
}
sem = new Semaphore(size);
}
// 利用对象池的对象,调用func
R exec(Function<T,R> func) {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool =
new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
ReadWriteLock
ReentrantReadWriteLock特点
- 在非公平模式下,进入读锁和写锁的顺序取决于可重入约束,是不确定的。
- 在非公平模式下,连续争用可能会导致一个或者多个读线程或写线程进入无限等待状态。
- 不过,通常非公平锁会比公平锁有更高的吞吐量。
- 在公平模式下,进入读锁和写锁的顺序使用一种近乎顺序的策略。
- 当前持有的锁被释放时,等待时间最长的单个写线程会被授予写锁,或者如果有一组读线程比所有的写线程等待的时间都长,则这组读线程将被授予锁。
- 如果有写线程持有锁或者有写线程正在等待锁,试图去获取一个公平的读锁(不可重入)的读线程将被阻塞。
- 这个读线程不会获得锁,直到当前等待的所有写线程获取并释放了锁。
- 当然,如果写线程放弃了等待,使得等待队列中只剩下一个或者多个等待时间最长的读线程,并且当前读锁可用,则这些读线程将会被授予锁。
- 除非当前的读锁和写锁都是可用的,写线程尝试去获取一个公平的写锁(不可重入)才不会被阻塞。
快速实现一个缓存
class Cache<K,V> {
final Map<K, V> m =
new HashMap<>();
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
// 读缓存
V get(K key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
// 写缓存
V put(K key, V value) {
w.lock();
try { return m.put(key, v); }
finally { w.unlock(); }
}
}
读写锁的升级与降级
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
//写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock(); ①
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {use(data);}
finally {r.unlock();}
}
}
StampedLock
final StampedLock sl =
new StampedLock();
// 获取/释放悲观读锁示意代码
long stamp = sl.readLock();
try {
//省略业务相关代码
} finally {
sl.unlockRead(stamp);
}
// 获取/释放写锁示意代码
long stamp = sl.writeLock();
try {
//省略业务相关代码
} finally {
sl.unlockWrite(stamp);
}
CountDownLatch
CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
CountDownLatch常用方法
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
使用示例
假设我们有这么一个需求,需要 1查询未对账订单和 2查询派件单,然后根据这两笔单子的结果去 3执行对账操作。要是串行操作的话,那执行流程是 1-》2-》3.为了提高执行效率,1,2我们可以并行操作,等待1,2都执行结束后,再根据1,2的执行结果执行步骤3的对账操作。根据前面对CountDownLatch的介绍,我们可以这样操作:
// 创建2个线程的线程池
Executor executor =
Executors.newFixedThreadPool(2);
while(存在未对账订单){
// 计数器初始化为2
CountDownLatch latch =
new CountDownLatch(2);
// 查询未对账订单
executor.execute(()-> {
pos = getPOrders();
latch.countDown();
});
// 查询派送单
executor.execute(()-> {
dos = getDOrders();
latch.countDown();
});
// 等待两个查询操作结束
latch.await();
// 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}
CyclicBarrier
int index = barrier.await();
if (index == 0) {
// 执行额外的操作
}
// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池
Executor executor =
Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
new CyclicBarrier(2, ()->{
executor.execute(()->check());
});
void check(){
P p = pos.remove(0);
D d = dos.remove(0);
// 执行对账操作
diff = check(p, d);
// 差异写入差异库
save(diff);
}
void checkAll(){
// 循环查询订单库
Thread T1 = new Thread(()->{
while(存在未对账订单){
// 查询订单库
pos.add(getPOrders());
// 等待
barrier.await();
}
});
T1.start();
// 循环查询运单库
Thread T2 = new Thread(()->{
while(存在未对账订单){
// 查询运单库
dos.add(getDOrders());
// 等待
barrier.await();
}
});
T2.start();
}