关于Java多线程和并发运行的内容学习了几天,也快结束了,这是第三篇,还剩一篇,就可以结束了。
上一篇的学习内容是线程池,Lock以及读写锁,缓存系统的实现原理。
这一次主要是学习五个API,解决一些实际的问题。分别是:Condition,Semaphore,CyclicBarrier,CountDownLatch,Exchanger。
由于每个API需要记录的东西很多,在这里将第三篇拆分成五小篇,分别讲解,这样容易后期翻阅。
一,关于Condition:
Condition的功能类似在传统线程技术中的Object.wait和Object.notify的功能。
在等待Condition时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为Condition应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议 应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
一个锁内部可以有多个Condition,即有多路等待和通知,可以参看jdk1.5提供的Lock与Condition实现的可阻塞队列的应用案例,从中除了要体味算法,还要体味面向对象的封装。在传统的线程机制中一个监视器对象上只能有一路等待和通知,要想实现 多路等待和通知,必须嵌套使用多个同步监视器对象。(如果只用一个Condition,两个放的都在等,一旦一个放进去了,那么它通知可能会导致另一个放接着往下走)
Condition
将 Object
监视器方法(wait
、notify
和notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意Lock
实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock
替代了synchronized
方法和语句的使用,Condition
替代了 Object 监视器方法的使用。
Condition
实例实质上被绑定到一个锁上。要为特定 Lock
实例获得Condition
实例,请使用其newCondition()
方法。
作为一个示例,假定有一个绑定的缓冲区,它支持 put
和 take
方法。
如果试图在空的缓冲区上执行 take
操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行put
操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put
线程和 take
线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。
可以使用两个 Condition
实例来做到这一点。
<span style="font-size:14px;"> class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
</span>
(
ArrayBlockingQueue
类提供了这项功能,因此没有理由去实现这个示例类。)
Condition
实现可以提供不同于 Object
监视器方法的行为和语义,比如受保证的通知排序,或者在执行通知时不需要保持一个锁。如果某个实现提供了这样特殊的语义,则该实现必须记录这些语义。
注意,Condition
实例只是一些普通的对象,它们自身可以用作 synchronized
语句中的目标,并且可以调用自己的wait
和notification
监视器方法。获取 Condition
实例的监视器锁或者使用其监视器方法,与获取和该Condition
相关的Lock
或使用其 waiting
和 signalling
方法没有什么特定的关系。为了避免混淆,建议除了在其自身的实现中之外,切勿以这种方式使用Condition
实例。
除非另行说明,否则为任何参数传递 null
值将导致抛出 NullPointerException
。
* 题目:在1子程序里面运行10次,在2子程序里面运行20次,然后在主程序里运行100次,
* 然后再回到1子程序里面运行10次,2子程序里面运行20次,主程序里面运行100次……如此反复运行50次。
* 写出程序。
*/
<span style="font-size:14px;">package Multithreading;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreeConditionCommunication {
public static void main(String[] args) {
final Business business = new Business();
new Thread(new Runnable() {// 线程只是调用包装好的Business类
public void run() {
for (int i = 1; i <= 50; i++) {
business.sub2(i);
}
}
}).start();
new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 50; i++) {
business.sub3(i);
}
}
}).start();
for (int i = 1; i <= 50; i++) {
business.main(i);
}
}
static class Business {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int bShouldSub = 1;
public void sub2(int i) {
lock.lock();
try {
while (bShouldSub != 2) {// 此处也可以用if,不过while更好,while是判断两次,程序更加健硕
try {
condition2.await();
} catch (Exception e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
System.out.println("sub2 thread sequece of " + j
+ ",loop of " + i);
}
bShouldSub = 3;
condition3.signal();
} finally {
lock.unlock();
}
}
public void sub3(int i) {
lock.lock();
try {
while (bShouldSub != 3) {// 此处也可以用if,不过while更好,while是判断两次,程序更加健硕
try {
condition3.await();
} catch (Exception e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 20; j++) {
System.out.println("sub3 thread sequece of " + j
+ ",loop of " + i);
}
bShouldSub = 1;
condition1.signal();
} finally {
lock.unlock();
}
}
public void main(int i) {
lock.lock();
try {
while (bShouldSub != 1) {
try {
condition1.await();
} catch (Exception e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
System.out.println("main thread sequece of " + j
+ ",loop of " + i);
}
bShouldSub = 2;
condition2.signal();
} finally {
lock.unlock();
}
}
}
}
</span>
注意事项:
在等待 Condition
时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为Condition
应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
三种形式的条件等待(可中断、不可中断和超时)在一些平台上的实现以及它们的性能特征可能会有所不同。尤其是它可能很难提供这些特性和维护特定语义,比如排序保证。更进一步地说,中断线程实际挂起的能力在所有平台上并不是总是可行的。
因此,并不要求某个实现为所有三种形式的等待定义完全相同的保证或语义,也不要求其支持中断线程的实际挂起。
要求实现清楚地记录每个等待方法提供的语义和保证,在某个实现不支持中断线程的挂起时,它必须遵从此接口中定义的中断语义。
由于中断通常意味着取消,而又通常很少进行中断检查,因此实现可以先于普通方法的返回来对中断进行响应。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。
再补充一个例子,关于图片加载的例子:
package Day03;
/**
* wait() notify方法
* @author Administrator
*
*/
public class WaitAndNotifyDemo {
public static boolean finish = false;
public static void main(String[] args) {
/**
* 两个线程并发运行 一个线程用于下载图片 另一个线程用于显示图片
* 这里就出现一个问题,显示图片的线程应当等待下载图片的线程将图片下载后再进行显示
*/
// 下载图片的线程
final Thread downLoadThread = new Thread(new Runnable() {
public void run() {
System.out.println("开始下载图片。。。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("图片下载完毕!");
finish = true;
/**
* 通知在当前对象上等待的线程回到runnable状态
* 这里的this 就是downLoadThread
* 而下面的displayTread就是在当前对象上等待的所以调用this.notify()方法会将
* displayThread解除等待阻塞.,使其可以继续运行
*/
synchronized (this) {
this.notify();//将this(downLoadThread)线程还原到原状态
}
}
});
// 显示图片的线程
Thread disPlayThread = new Thread(new Runnable() {
public void run() {
System.out.println("等待下载图片。。。");
try {
/**
* 当显示线程通过调用一个对象的wait()方法后,那么这个线程就在当前线程上等待,进入了 等待阻塞wait
* block 等待阻塞与睡眠阻塞sleep block 的区别在于:
* sleep()阻塞会在指定时间消耗完毕后自动回到runnable状态
* wait()阻塞不会自动回到runnable,知道调用了这个对象的notify()方法,当前线程
* 才会回到runnable状态
*/
synchronized (downLoadThread) {
/**
* 当前线程在哪个对象上进行等待,就需要哪个对象的锁
*/
downLoadThread.wait();
}
Thread.sleep(2000);
} catch (InterruptedException e) {
}
if (!finish) {
System.out.println("图片下载失败,无法显示。。。");
} else {
System.out.println("开始显示图片!");
}
}
});
downLoadThread.start();
disPlayThread.start();
}
}
运行结果: