本博客转载自《奔跑的猪的博客》
本文中的代码可以在github上找到,有需要的请自行下载

标题

定义

维基百科中定义管程为:在并发编程中,管程(monitor)为一个同步结构,具有线程互斥特性,以及能够根据某些条件来阻塞线程。根据定义,管程有三个要素:同步、互斥、条件。恰好在Java的Concurrent包中ReentrantLock具有上述所有特性,可以用来实现管程。管程是一个非常实用且常见的技术,可以用来实现很多常用的并发数据结构,例如阻塞队列。

阻塞队列

队列常用于生产者消费者模型,生产者发送消息并存储到队列,消费者从队列中取出消息。一般情况下会存在多个生产者和多个消费者,普通的队列不能保证并发的安全,因此需要用到线程安全的技术。线程安全的队列又可以分为两种:

  • 阻塞队列(BlockingQueue)
  • 非阻塞队列(NoBlockingQueue)

阻塞队列的特征为,当队列为空时会阻塞消费者,当队列满时阻塞生产者。这种机制能够平衡生产者和消费者的负载。

分析上述定义,阻塞队列具有线程安全,阻塞,条件的特性;线程安全和阻塞就意味着要实现同步以及互斥,因此,管程能够很好地满足阻塞队列的要求。

Java源码中的管程

如下代码所示是Concurrent包中ArrayBlockingQueue的实现,队列有两种实现方式,数组和链表。Array是数组的实现,而数组通常是有界的。

ArrayBlockingQueue结构将ReentrantLock(可重入锁)作为全局锁来实现线程安全,所谓全局锁就是整个数据结构中就这么一个锁,当一个线程在访问该对象并获得锁之后,其他线程要访问该对象都得阻塞。在后续的学习中,我们会发现全局锁有一定的弊端,因为他锁住了整个对象,使得整体的并发性不高。源代码中的newCondition()操作会生成一个条件对象,条件对象具有唤醒线程和挂起线程的能力,具体的操作如第二段代码所示。

public ArrayBlockingQueue(int capacity, boolean fair) {if (capacity <= 0)throw new IllegalArgumentException();this.items = new Object[capacity];lock = new ReentrantLock(fair);notEmpty = lock.newCondition();notFull =  lock.newCondition();
}

第二段代码是ArrayBlockingQueue的put操作,put操作的第一步就是获得全局锁,然后判断队列当前元素是否已满,如果队列慢就会使用notFull条件变量将线程挂起,否则就会调用enqueue函数进行入队操作。入队操作同时会使用notEmpty条件变量来唤醒一个被notEmpty阻塞的的线程(take操作会调用notEmpty来阻塞线程)。

public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {while (count == items.length)notFull.await();//挂起线程,当其他线程调用notFull.signal()或者notFull.signlAll()时,该线程唤醒。enqueue(e);} finally {lock.unlock();}}
private void enqueue(E x) {// assert lock.getHoldCount() == 1;// assert items[putIndex] == null;final Object[] items = this.items;items[putIndex] = x;if (++putIndex == items.length)putIndex = 0;count++;notEmpty.signal();//唤醒一个被notEmpty阻塞的线程,因为此时队列已经非空了}

实例

下面我为了熟悉管程的使用,会亲自造轮子体会一下。

package Monitors;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class BlockingQueue<E> {private ReentrantLock lock;//全局锁private Condition notFull;//条件变量,用来监控队列是否已满private Condition notEmpty;//条件变量,用来监控队列是否为空private final int capacity;//容量,这是一个定容的队列private int head;//队列头部哨兵private int tail;//队列尾部哨兵private int count;//当然元素个数private E[] data;//保存元素的数组/*初始化阻塞队列*/public BlockingQueue(int c){this.lock=new ReentrantLock();this.notEmpty=lock.newCondition();this.notFull=lock.newCondition();this.capacity=c;this.count=0;this.head=0;this.tail=0;data=(E[])new Object[c+1];}/*向队列中添加一个元素*/public void put(E e){lock.lock();try{while(this.count==this.capacity)//判断队列是否已满notFull.await();//挂起线程tail++;count++;if(tail==this.capacity+1)tail=0;data[tail]=e;//插入元素notEmpty.signal();//通知其他线程,队列不为空} catch (InterruptedException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}finally{lock.unlock();}}/*获取队列容量*/public int getCap(){return this.capacity;}/*判读队列是否为空*/public boolean isEmpty(){lock.lock();try{return count==0?true:false;}finally{lock.unlock();}}/*判断队列是否已满*/public boolean isFull(){lock.lock();try{return count==capacity?true:false;}finally{lock.unlock();}}/*从队列中取出一个元素*/public E take() throws InterruptedException{lock.lock();try{while(this.count==0)//判断队列是否为空notEmpty.await();//阻塞队列head++;count--;if(head==this.capacity+1)head=0;E x=data[head];//获得头部元素notFull.signal();//通知其他线程,队列未满return x;}finally{lock.unlock();}}
}

在上锁所有操作中,几乎每个操作都在如下的结构体中进行,为了防止代码异常退出而导致锁没有被释放,必须使用finally关键字确保锁的释放。该代码块的作用和synchronized同步块作用类似,在进入同步块之前都要先获得锁,然后进行同步操作;在退出同步块的时候都要释放锁并再次同步。

lock.lock();
try{return count==capacity?true:false;
}finally{lock.unlock();
}

参考文献

下面两本书我极力推荐,它们对并发编程解释得非常全面。

[1] Brian Goetz等著 童云兰译..机械工业出版社
[2][Maurice Herlihy, Nir Shavit著,<多处理器编程的艺术]

Java并发编程之(二)管程相关推荐

  1. [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors...

    [Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...

  2. java并发编程(二十一)----(JUC集合)CopyOnWriteArraySet和ConcurrentSkipListSet介绍

    转载自  java并发编程(二十一)----(JUC集合)CopyOnWriteArraySet和ConcurrentSkipListSet介绍 这一节我们来接着介绍JUC集合:CopyOnWrite ...

  3. Java并发编程(二)- 分工

    目录 分工问题 线程池 Java线程池的基本用法 线程池添加线程的逻辑 线程池的重要参数:工作队列 线程池的重要参数:拒绝策略 创建线程池的快捷方法 线程数量 线程池使用原则 获取线程执行结果 - F ...

  4. Java并发编程(二)同步

    1. 锁对象 Synchronized synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C. D等)正在用这个方 ...

  5. java并发编程(二)synchronized

    参考文章: http://blog.csdn.net/javazejian/article/details/72828483 http://ifeve.com/java-synchronized/ h ...

  6. Java 并发编程(二)对象的发布逸出和线程封闭

    对象的发布与逸出 "发布(Publish)"一个对象是指使对象能够在当前作用域之外的代码中使用.可以通过 公有静态变量,非私有方法,构造方法内隐含引用 三种方式. 如果对象构造完成 ...

  7. java并发编程(二)多个线程多个锁

    多个线程多个锁 多个线程多个锁:多个线程,每个线程都可以拿到自己制定的锁,分别获得锁之后,执行synchronized方法体的内容.就是在上次那个博客上说道的锁竞争的问题,是因为所有的线程过来以后都争 ...

  8. java并发编程学习一

    java并发编程学习一 什么是进程和线程? 进程是操作系统进行资源分配的最小单位 进程跟进程之间的资源是隔离的,同一个进程之中的线程可以共享进程的资源. 线程是进程的一个实体,是CPU 调度和分派的基 ...

  9. Java并发编程(三)volatile域

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Android多线程(一)线程池 Android多线程(二)AsyncTask源代码分析 前言 有时仅仅为了读写一个或 ...

  10. Java并发编程最佳实例详解系列

    Java并发编程最佳实例详解系列: Java并发编程(一)线程定义.状态和属性 Java并发编程(一)线程定义.状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元.在java语言中, ...

最新文章

  1. lintcode 滑动窗口的最大值(双端队列)
  2. 开发者需要了解的WebKit
  3. 算法笔记——整数划分1
  4. 问题解决: 解释器错误: 没有那个文件或目录
  5. Scrapy框架的学习(9.Scrapy中的CrawlSpider类的作用以及使用,实现优化的翻页爬虫)
  6. java 数组长度 可变_java基础之集合长度可变的实现原理
  7. 洛谷 P1377 [TJOI2011]树的序 解题报告
  8. 动态规划求解所有字符的组合数
  9. UISegmentedControl UISlider
  10. 实践教程 | Pytorch 模型的保存与迁移
  11. android 图片 写入文件格式,android实现将位置信息写入JPEG图片文件
  12. 打造金刚不坏之心的秘籍
  13. java swing 汽车租赁管理系统 java swing mysql实现的汽车租赁管理系统源码(1027)
  14. 开始学习机器学习之前你必须要了解的知识有哪些?机器学习系列入门篇
  15. 将日/分钟数据转为周线、月线或其他周期
  16. linux docker启动指定字符集,【字符集】解决docker 容器中中文乱码问题
  17. Dynamics 365 窗体中设置可编辑的子网格
  18. rs_D455相机内外参标定+imu联合标定
  19. 【springboot进阶】RestTemplate 集成 okhttp3 请求带p12证书
  20. 光纤收发器的6个指示灯说明

热门文章

  1. 电脑怎么写入便签并同步到手机版便签上?
  2. 计算机一打开就卡在更新失败,做系统一直在正在启动画面-电脑开机后卡在“正在启动windows”界面,怎么办?...
  3. 汉语言专家级C1,汉语言文学专业审核(文科生均可参考)
  4. 北京/上海内推 | 小红书社区技术部招聘NLP/多模态算法工程师/实习生
  5. 破解EXCEL工作表保护密码
  6. web学习-项目练习-No.4-朋友圈
  7. linux中dd命令详解,Linux基础知识之dd命令详解
  8. linux dd查看磁盘读写,使用dd命令 测试磁盘读写速度
  9. 等级保护二、三、四级内容及对比
  10. OSChina 周一乱弹 ——强行把她拖到家里洗了个澡