Java并发——线程间通信与同步技术
传统的线程间通信与同步技术为Object上的wait()、notify()、notifyAll()等方法,Java在显示锁上增加了Condition对象,该对象也可以实现线程间通信与同步。本文会介绍有界缓存的概念与实现,在一步步实现有界缓存的过程中引入线程间通信与同步技术的必要性。首先先介绍一个有界缓存的抽象基类,所有具体实现都将继承自这个抽象基类:
public abstract class BaseBoundedBuffer<V> {
private final V[] buf;
private int tail;
private int head;
private int count;
protected BaseBoundedBuffer(int capacity) {
this.buf = (V[]) new Object[capacity];
}
protected synchronized final void doPut(V v) {
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
++count;
}
protected synchronized final V doTake() {
V v = buf[head];
buf[head] = null;
if (++head == buf.length)
head = 0;
--count;
return v;
}
public synchronized final boolean isFull() {
return count == buf.length;
}
public synchronized final boolean isEmpty() {
return count == 0;
}
}
在向有界缓存中插入或者提取元素时有个问题,那就是如果缓存已满还需要插入吗?如果缓存为空,提取的元素又是什么?以下几种具体实现将分别回答这个问题。
1、将异常传递给调用者
最简单的实现方式是:如果缓存已满,向缓存中添加元素,我们就抛出异常:
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
public GrumpyBoundedBuffer() {
this(100);
}
public GrumpyBoundedBuffer(int size) {
super(size);
}
public synchronized void put(V v) throws BufferFullException {
if (isFull())
throw new BufferFullException();
doPut(v);
}
public synchronized V take() throws BufferEmptyException {
if (isEmpty())
throw new BufferEmptyException();
return doTake();
}
}
这种方法实现简单,但是使用起来却不简单,因为每次put()与take()时都必须准备好捕捉异常,这或许满足某些需求,但是有些人还是希望插入时检测到已满的话,可以阻塞在那里,等队列不满时插入对象。
2、通过轮询与休眠实现简单的阻塞
当队列已满插入数据时,我们可以不抛出异常,而是让线程休眠一段时间,然后重试,此时可能队列已经不是已满状态:
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
int SLEEP_GRANULARITY = 60;
public SleepyBoundedBuffer() {
this(100);
}
public SleepyBoundedBuffer(int size) {
super(size);
}
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty())
return doTake();
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}
这种实现方式最大的问题是,我们很难确定合适的休眠间隔,如果休眠间隔过长,那么程序的响应性会变差,如果休眠间隔过短,那么会浪费大量CPU时间。
3、使用条件队列实现有界缓存
使用休眠的方式会有响应性问题,因为我们无法保证当队列为非满状态时线程就会立刻sleep结束并且检测到,所以,我们希望能有另一种实现方式,当缓存非满时,会主动唤醒线程,而不是需要线程去轮询缓存状态,Object对象上的wait()与notifyAll()能够实现这个需求。当调用wait()方法时,线程会自动释放锁,并请求请求操作系统挂起当前线程;当其他线程检测到条件满足时,会调用notifyAll()方法唤醒挂起线程,实现线程间通信与同步:
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
public BoundedBuffer() {
this(100);
}
public BoundedBuffer(int size) {
super(size);
}
public synchronized void put(V v) throws InterruptedException {
while (isFull())
wait();
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException {
while (isEmpty())
wait();
V v = doTake();
notifyAll();
return v;
}
public synchronized void alternatePut(V v) throws InterruptedException {
while (isFull())
wait();
boolean wasEmpty = isEmpty();
doPut(v);
if (wasEmpty)
notifyAll();
}
}
注意,上面的例子中我们使用了notifyAll()唤醒线程而不是notify()唤醒线程,如果我们改用notify()唤醒线程的话,将导致错误的,notify()会在等待队列中随机选择一个线程唤醒,而notifyAll()会唤醒所有等待线程。对于上面的例子,如果现在是非满状态,我们使用notify()唤醒线程,由于只能唤醒一个线程,那么我们唤醒的可能是在等待非空状态的线程,将导致信号丢失。只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll:
- 所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
- 单进单出。在条件变量上的每次通知,最多只能唤醒一个线程来执行。
4、使用显示的Condition实现有界缓存
内置条件队列存在一些缺陷,每个内置锁都只能有一个相关联的条件队列,因而像上个例子,多个线程都要在同一个条件队列上等待不同的条件谓词,如果想编写一个带有多个条件谓词的并发对象,就可以使用显示的锁和Condition,与内置锁不同的是,每个显示锁可以有任意数量的Condition对象。以下代码给出了有界缓存的另一种实现,即使用两个Condition,分别为notFull和notEmpty,用于表示"非满"与"非空"两个条件谓词。
public class ConditionBoundedBuffer<T> {
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private static final int BUFFER_SIZE = 100;
private final T[] items = (T[]) new Object[BUFFER_SIZE];
private int tail, head, count;
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[tail] = x;
if (++tail == items.length)
tail = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
T x = items[head];
items[head] = null;
if (++head == items.length)
head = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
注意,在上面的例子中,由于使用了两个Condition对象,我们的唤醒方法调用的是signal()方法,而不是signalAll()方法。
使用条件队列时,需要特别注意锁、条件谓词和条件变量之间的三元关系:在条件谓词中包含的变量必须由锁保护,在检查条件谓词以及调用wait和notify(或者await和signal)时,必须持有锁对象。
Java并发——线程间通信与同步技术的更多相关文章
- Java多线程——线程间通信
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...
- Java并发--线程间协作的两种方式:wait、notify、notifyAll和Condition
在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界 ...
- Java并发——线程间的等待与通知
前言: 前面讲完了一些并发编程的原理,现在我们要来学习的是线程之间的协作.通俗来说就是,当前线程在某个条件下需要等待,不需要使用太多系统资源.在某个条件下我们需要去唤醒它,分配给它一定的系统资源,让它 ...
- Java多线程:线程间通信之volatile与sychronized
由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...
- Java 里如何实现线程间通信
正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点:thread.join(), object.w ...
- Java 如何实现线程间通信
正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点: thread.join(), object. ...
- 有多少人在面试时,被Java 如何线程间通讯,问哭了?
正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点: thread.join(), object. ...
- Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)
一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...
- Java并发——使用Condition线程间通信
线程间通信 线程之间除了同步互斥,还要考虑通信.在Java5之前我们的通信方式为:wait 和 notify.Condition的优势是支持多路等待,即可以定义多个Condition,每个condit ...
随机推荐
- 快速排序和二分查找(Go)
package main import ( "fmt" "sync") var waitGroup sync.WaitGroup func main() { d ...
- async await task.Result 卡死
在如下代码中: public async Task<string> GetData() { return await DoWork(); } 在UI线程中调用 var data = Get ...
- selenium报错汇总
selenium报错汇总 报错:[error] Could not connect to Selenium Server. Have you started the Selenium Server y ...
- Flink集群环境搭建
环境准备 master:171:slave:171,172:flink版本:1.3.0 下载地址:http://archive.apache.org/dist/flink/flink-1.3.0/ 集 ...
- MySQL5.6版本之后设置DATETIME类型自动更新
在使用MySQL中datetime格式自动更新特性时,我们应该明确一点,datetime格式设置默认值为当前时间和自动更新时间是从MySQL5.6版本之后开始支持的.此前我们都是使用timestamp ...
- NW.js
1.package.json属性说明: ——window窗口外观常用属性包括: title : 字符串,设置默认 title width/height : 主窗口的大小 toolbar : bool ...
- JavaWeb项目:Shiro实现简单的权限控制(整合SSM)
该demo整合Shiro的相关配置参考开涛的博客 数据库表格相关设计 表格设计得比较简单,导航栏直接由角色表auth_role的角色描述vRoleDesc(父结点)和角色相关权限中的权限描述(标记为 ...
- ES6标准入门 第五章:正则的扩展
1.RegExp 构造函数 ES5的缺陷: ES5中构造函数的参数有两种情况: (1)参数是字符串,这时第二个参数表示正则表达式的的修饰符(flag). var reg = new RegExp( & ...
- Android专项测试监控资源
版本号 V 1.1.0 Android性能测试分为两类:1.一类为rom版本(系统)的性能测试2.一类为应用app的性能测试(本次主要关注点为app的性能测试) Android的app性能测试包括的测 ...
- java文件编译后,出现xx$1.class的原因
java编译后的文件名字带有$接数字的就是匿名内部类的编译结果,接名字的就是内部类的编译结果 例如:TestFrame$1.class是匿名内部类的编译结果, TestFrame$MyJob.clas ...