Java多线程总结2
同步装置
JDK5.0后提供了一个新的多线程并发控制的装置/工具,它允许一组线程互相进行协调运行。
先引用IBM网站的一个表格:
Semaphore |
一个经典的并发工具(计数信号量)。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。 |
CountDownLatch |
同步辅助类,在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直等待。用于在保持给定数目的信号、事件或条件前阻塞执行。 |
CyclicBarrier |
同步辅助类,是一个可重置的多路同步点,在某些并行编程风格中很有用。 |
Exchanger |
允许两个线程在 collection 点交换对象,它在多流水线设计中是有用的。 |
1.信号量
Semaphore 信号量通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。Semaphore可以看成是个通行证,线程访问资源池中的资源时必须先取得通行证。如果线程没有取得通行证就会被阻塞进入等待状态。
通常在取得通行证前会调用acquire()阻塞其他的申请者,然后再获取该许可。最后调用 release() 释放一个通行证,从而也可能释放了一个正在阻塞的申请者。
有两个主要的方法:
* public void acquire();
* public void release();
Semaphore提供的通行证数量和资源池的大小一致。
注意的是:信号量仅仅是对池资源进行监控,但不保证线程的安全,因此,在使用时候,应该自己控制线程的安全访问池资源。
网上拷贝的一个示例:
public class Pool {
ArrayList pool = null;
Semaphore pass = null;
public Pool(int size){
//初始化资源池
pool = new ArrayList();
for(int i=0; i
pool.add("Resource "+i);
}
//Semaphore的大小和资源池的大小一致
pass = new Semaphore(size);
}
public String get() throws InterruptedException{
//获取通行证,只有得到通行证后才能得到资源
pass.acquire();
return getResource();
}
public void put(String resource){
//归还通行证,并归还资源
pass.release();
releaseResource(resource);
}
private synchronized String getResource() {
String result = pool.get(0);
pool.remove(0);
System.out.println("Give out "+result);
return result;
}
private synchronized void releaseResource(String resource) {
System.out.println("return "+resource);
pool.add(resource);
}
}
2.同步计数器
同步计数器CountDownLatch的实现是通过一个计数器来控制线程对资源的访问,当计数器不为零时处于等待中。它可用于一个简单的开/关锁存器或入口,也可用于分解计算一个复杂问题。它主要有两个方法:
public void countDown()
递减锁存器的计数,如果计数到达零,则释放所有等待的线程。如果当前计数大于零,则将计数减少。如果新的计数为零,出于线程调度目的,将重新启用所有的等待线程。如果当前计数等于零,则不发生任何操作。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException
使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。如果当前计数为零,则此方法立刻返回 true 值。
引用一个例子:
class Driver2 {
void main() throws InterruptedException {
int N = 10;
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = Executors.newSingleThreadExecutor();
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // 等待所有任务结束(即计数器为0)
………………………..
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
doneSignal.countDown(); //计数器递减
} catch (Exception ex) {
} // return;
}
void doWork(int num) {
System.out.println(num);
}
}
}
3.障碍器
障碍器CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
CyclicBarrier与CountDownLatchu有些相似,但也存在一些区别:
CountDownLatchu是等待一组线程执行完以后,再执行等待的线程,等待线程可以是一个或几个线程;通过计数器来通信。CyclicBarrier是等待多个子线程都执行到某一点后,再继续执行CyclicBarrier中的任务。
有一个主要方法:
public int await();
引用一个网上的例子:两个线程分别在一个数组里放一个数,当这两个线程都结束后,主线程算出数组里的数的和
public class Test {
public static void main(String[] args) {
//创建障碍器,并设置MainTask为所有定数量的线程都达到障碍点时候所要执行的任务(Runnable)
CyclicBarrier cb = new CyclicBarrier(7, new MainTask());
new SubTask("A", cb).start();
new SubTask("B", cb).start();
new SubTask("C", cb).start();
new SubTask("D", cb).start();
new SubTask("E", cb).start();
new SubTask("F", cb).start();
new SubTask("G", cb).start();
}
}
/**
* 主任务
*/
class MainTask implements Runnable {
public void run() {
System.out.println(">>>>主任务执行了!<<<<");
}
}
/**
* 子任务
*/
class SubTask extends Thread {
private String name;
private CyclicBarrier cb;
SubTask(String name, CyclicBarrier cb) {
this.name = name;
this.cb = cb;
}
public void run() {
System.out.println("[子任务" + name + "]开始执行了!");
for (int i = 0; i < 999999; i++) ; //模拟耗时的任务
System.out.println("[子任务" + name + "]开始执行完成了,并通知障碍器已经完成!");
try {
//通知障碍器已经完成
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
执行结果:
[子任务E]开始执行了!
[子任务E]开始执行完成了,并通知障碍器已经完成!
[子任务F]开始执行了!
[子任务G]开始执行了!
[子任务F]开始执行完成了,并通知障碍器已经完成!
[子任务G]开始执行完成了,并通知障碍器已经完成!
[子任务C]开始执行了!
[子任务B]开始执行了!
[子任务C]开始执行完成了,并通知障碍器已经完成!
[子任务D]开始执行了!
[子任务A]开始执行了!
[子任务D]开始执行完成了,并通知障碍器已经完成!
[子任务B]开始执行完成了,并通知障碍器已经完成!
[子任务A]开始执行完成了,并通知障碍器已经完成!
>>>>主任务执行了!<<<<
4.连接器
连接器Exchanger类,可以用来完成线程间的数据交换。当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。Exchanger仅可以在两个线程之间交换数据。
Exchanger 可能被视为 SynchronousQueue 的双向形式。Exchanger 可能在应用程序(比如遗传算法和管道设计)中很有用。
引个网上的例子:
class FillAndEmpty {
//初始化一个Exchanger,并规定可交换的信息类型是DataBuffer
Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
DataBuffer initialEmptyBuffer = new DataBufferByte(10, 10); //初始化一个empty Buffer
DataBuffer initialFullBuffer = new DataBufferByte(10, 10); //初始化一个full Buffer
class FillingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialEmptyBuffer;
try {
while (currentBuffer != null) {
addToBuffer(currentBuffer); //往empty Buffer添加数据
if (currentBuffer.isFull())
currentBuffer = exchanger.exchange(currentBuffer); //和full Buffer进行交换
}
} catch (InterruptedException ex) {
//...handle...
ex.printStackTrace();
}
}
}
class EmptyingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialFullBuffer;
try {
while (currentBuffer != null) {
takeFromBuffer(currentBuffer); //从full Buffer中取数据
if (currentBuffer.isEmpty())
currentBuffer = exchanger.exchange(currentBuffer); //和empty Buffer进行交换
}
} catch (InterruptedException ex) {
//...handle...
ex.printStackTrace();
}
}
}
void start() {
new Thread(new FillingLoop()).start();
new Thread(new EmptyingLoop()).start();
}
public void addToBuffer(DataBuffer data) {
….
}
public void takeFromBuffer() {
….
}
}
当线程A调用Exchange对象的exchange()方法后,他会陷入阻塞状态,直到线程B也调用了exchange()方法,然后以线程安全的方式交换数据,之后线程A和B继续运行
原子变量与无阻塞算法
关于原子变量,可以参考IBM网站文章:
Java理论与实践:流行的原子 https://www.ibm.com/developerworks/cn/java/j-jtp11234/
原子变量是基于现代处理器(CPU)的硬件支持把两步操作合为一步,避免了不必要的锁定,提高了程序的运行效率。它们实现了CAS(compare-and-set)和 “check-and-act”动作。
Java中的常见的原子变量类(automic variable classes)主要有AtomicBoolean, AtomicInteger, AotmicIntegerArray, AtomicLong, AtomicLongArray, AtomicReference ……。
这些原子量级的变量主要提供两个方法:
* compareAndSet(expectedValue, newValue): 比较当前的值是否等于expectedValue,若等于把当前值改成newValue,并返回true。若不等,返回false。
* getAndSet(newValue): 把当前值改为newValue,并返回改变前的值。
Java中的原子变量更多地是被用在“非阻塞算法”或“lock-free算法”中。关于非阻塞算法可以参考IBM网站文章:Java 理论与实践: 非阻塞算法简介 http://www.ibm.com/developerworks/cn/java/j-jtp04186/
阻塞队列
java阻塞队列应用于生产者消费者模式、消息传递、并行任务执行和相关并发设计的大多数常见使用上下文。
BlockingQueue是一种特殊的Queue,若BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态直到BlocingkQueue进了新货才会被唤醒。同样,如果BlockingQueue是满的任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有新的空间才会被唤醒继续操作。
ConcurrentLinkedQueue |
一个基于链接节点的高效的、可伸缩的、线程安全的非阻塞 FIFO 队列, 此实现采用了有效的“无等待 (wait-free)”算法 |
LinkedBlockingQueue |
一个基于已链接节点的、范围任意的 blocking queue |
ArrayBlockingQueue |
一个由数组支持的有界阻塞队列。这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。 |
SynchronousQueue |
一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。 |
PriorityBlockingQueue |
一个无界阻塞队列,类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。 |
DelayQueue |
一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。 |
LinkedBlockingDeque |
一个基于已链接节点的、任选范围的阻塞双端队列。是BlockingDeque接口的一个基本实现,以支持 FIFO 和 LIFO(基于堆栈)操作。 |
并发集合
在Java的聚集框架里可以调用Collections.synchronizeCollection(aCollection)将普通聚集改变成同步聚集,使之可用于多线程的环境下。 但同步聚集在一个时刻只允许一个线程访问它,其它想同时访问它的线程会被阻断,导致程序运行效率不高。Java 5.0里提供了几个共点聚集类,它们把以前需要几步才能完成的操作合成一个原子量级的操作,这样就可让多个线程同时对聚集进行操作,避免了锁定,从而提高了程序的运行效率。JDK 1.5提供下面一些集合实现,它们是被设计为用于多线程环境的:
ConcurrentHashMap |
为检索和更新(update)可调整的预期的并发性提供了完整的线程安全的(thread-safe)并发性支持 |
CopyOnWriteArrayList |
一组线程安全的变量集合 |
CopyOnWriteArraySet |
一个线程安全的数组列表(ArrayList)变量 |
ConcurrentSkipListMap |
对应TreeMap |
ConcurrentSkipListSet |
|
在修改原始的数组或集合之前,它们中的每一个都会把下层的数组或集合复制一份。其结果是,读取的速度很快,而更新的速度很慢。
并发集合类为Iterator(迭代子)提供快照式的数据(即使下层数据发生了改变,在Iterator中也不会反映出来)。
IBM Java多线程与并发编程专题 http://www.ibm.com/developerworks/cn/java/j-concurrent/