一. 同步工具类
同步容器、并发容器可以保证容器内数据被正确访问,但是线程访问流还是不受制约的。同步工具类产生的原因就是协调线程的控制流。
某种程度上来说,阻塞队列既是并发容器类,也是同步工具类。阻塞队列对缓存读取、写入的线程操作进行了延时阻塞,也达到了控制线程访问流的目的。但是阻塞队列终究是从容器角度出发,对和容器无关的其他线程流并未做很好的限制。因而同步工具类有了其发挥的余地。
常用的同步工具类包括闭锁、信号量和栅栏。
二. 闭锁(CountDownLatch)
闭锁通常使用一个计数器,表示需要等待事件的数量。CountDownLatch是最常见灵活的闭锁实现:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public long plannedSchedule(int nThreads, final Runnable schedule) throws InterruptedException{
final CountDownLatch startFlag = new CountDownLatch(1);
final CountDownLatch endFlag = new CountDownLatch(nThreads);
for(int i=0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startFlag.await();
try {
schedule.run();
}finally {
endFlag.countDown();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
long startTime = System.nanoTime();
startFlag.countDown();
endFlag.await();
long endTime = System.nanoTime();
return endTime-startTime;
}
}
首先新建CountDownLatch对象,申请一个计数器初始值。调用countDown()函数会递减闭锁值,表示一个事件发生了;调用await()函数会查询该计数器,若为0表示所有等待事件都已发生,若不为0则一直阻塞到计数器为0(除非等待中的线程中断,或者超过等待时间)。
使用闭锁的原因是希望新开线程全部同时启动运行。否则,线程启动时间是根据调用start()的先后顺序决定,先启动的线程将“领先”后启动的线程。那么startFlag控制主线程同时“放行”所有的工作线程,endFlag则控制主线程只关注最后一个线程执行完成(不同线程执行不同Runnable耗时不同)。如果没有endFlag则主线程要轮询所有工作线程,顺序地等待每个线程执行完成。
三. 信号量(Semaphore)
计数信号量用于控制某个特定资源的操作数量。可以想象Semaphore计数器为虚拟许可(permit),虚拟许可的数量通过初始化构造函数指定。
1. 线程在执行操作时,先调用acquire( )函数获得许可(计数器自减1);如果此时许可没有剩余(计数器为0)则阻塞到有许可为止(除非被中断或者操作过时)。
2. 线程执行完后,调用release( )函数释放许可(计数器自增1);
3. 初始值为1的信号量称作二值信号量(只有可能0、1),可以用作不可重入的“互斥锁”。
可重入锁:以线程为粒度的锁,比如当前线程已持有锁不用重复申请。
不可重入锁:不可重入,以私有对象为粒度的锁,因为该对象的唯一性,线程即使持有锁仍须释放、再申请。
不可重入锁可以表达为下列例子:
public class ReentrantLockDemo {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked) {
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}
可重入锁以线程为粒度,可以表达为下列例子:
public class UnReentrantLockDemo {
private boolean isLocked = false;
Thread lockBy = null;
int l