传统的线程间通信与同步技术为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:

  1. 所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
  2. 单进单出。在条件变量上的每次通知,最多只能唤醒一个线程来执行。

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并发——线程间通信与同步技术的更多相关文章

  1. Java多线程——线程间通信

    Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...

  2. Java并发--线程间协作的两种方式:wait、notify、notifyAll和Condition

    在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界 ...

  3. Java并发——线程间的等待与通知

    前言: 前面讲完了一些并发编程的原理,现在我们要来学习的是线程之间的协作.通俗来说就是,当前线程在某个条件下需要等待,不需要使用太多系统资源.在某个条件下我们需要去唤醒它,分配给它一定的系统资源,让它 ...

  4. Java多线程:线程间通信之volatile与sychronized

    由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...

  5. Java 里如何实现线程间通信

    正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点:thread.join(), object.w ...

  6. Java 如何实现线程间通信

    正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点: thread.join(), object. ...

  7. 有多少人在面试时,被Java 如何线程间通讯,问哭了?

    正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点: thread.join(), object. ...

  8. Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)

    一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...

  9. Java并发——使用Condition线程间通信

    线程间通信 线程之间除了同步互斥,还要考虑通信.在Java5之前我们的通信方式为:wait 和 notify.Condition的优势是支持多路等待,即可以定义多个Condition,每个condit ...

随机推荐

  1. spring cloud:config-eureka-refresh

    config-server-eureka project 1. File-->new spring project 2.add dependency <parent> <gro ...

  2. Nor Flash芯片特性分析

    Nor Flash是Intel在1988年推出的非易失闪存芯片,可随机读取,擦写时间长,可以擦写1~100W次,支持XIP(eXecute In Place). 本文以JS28F512M29EWH为例 ...

  3. java dwg转svg

    package com.example.demo.dxf2svg; import com.aspose.cad.InterpolationMode; import com.aspose.cad.Smo ...

  4. 【mysql】查询最新的10条记录

    实现查询最新10条数据方法: select * from 表名 order by id(主键) desc limit 10 参考文档: MySQL查询后10条数据并顺序输出

  5. pandas库简介和数据结构

    pandas简介 pandas是一个强大的Python数据分析的工具包.是基于Numpy来构件的. pandas提供快速.灵活和富有表现力的数据结构. 主要功能: 具备对其功能的数据结构DataFra ...

  6. 未解决:found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details

    问题出现: 在通过 `ng new hello-world` 命令新建项目时,项目出现以下警告: found high severity vulnerability run `npm audit fi ...

  7. Gradle之Gradle 的基本使用(一)

    [Android 修炼手册]Gradle 篇 -- Gradle 的基本使用 预备知识 基本的 android 开发知识 了解 Android Studio 基本使用 看完本文可以达到什么程度 掌握 ...

  8. 多线程13-CountdownEvent

        );         ));             ));             t1.Start();             t2.Start();             _coun ...

  9. Nginx服务器优势是什么

    nginx介绍.功能,优势 https://www.cnblogs.com/wcwnina/p/8728391.html#!comments Nginx负载均衡,session共享问题,几种解决方案 ...

  10. 【C语言--数据结构】线性表链式存储结构

    直接贴代码 头文件 #ifndef __LINKLIST_H__ #define __LINKLIST_H__ typedef void LinkList; typedef struct _tag_L ...