并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章。

今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的例子来理解一下:

一条4车道的主干道,假设100米长,每辆车假设占用的长度为10米(考虑到前后车距),也就是说这条道上满负载运行的话,最多只能容纳4*(100/10)=40辆车,如果有120辆车要通过的话(为简单起见,一波40辆,分成3波),就必须要红绿信号灯来调度了,对于最前面的一波来讲,它们看到的是绿灯,允许通过,第一波全进入道路后,红绿灯变成红色,表示后面的2波,要停下来等候第1波车辆全通过,然后红绿灯才会变成绿色,让第2波通过,如此运转下去....

这跟多线程并发有啥关系呢?Semaphore就是红绿信号灯,3波车辆就是3个并发的线程,而主干道就是多个线程要并发访问的公用资源,由于资源有限,所以必须通过Semaphore来控制线程对资源的访问,否则就变成资源竞争,严重的话会导致死锁等问题。

下面用一个示例演示,假设有N个并发线程都要打印文件,但是打印机只有1台,先来一个打印队列类:

package yjmyzz.lesson01;

import java.util.concurrent.Semaphore;

public class PrintQueue {

    private final Semaphore semaphore;

    public PrintQueue() {
semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
} public void printJob(Object document) {
try {
semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙)) long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
}
}
}

由于是在多线程环境中,真正运行的作业处理,得继承自Runnable(或Callable)

package yjmyzz.lesson01;

public class Job implements Runnable {

    private PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
} public void run() {
System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
}

好了,测试一把:

package yjmyzz.lesson01;

public class Main {
public static void main(String args[]) { PrintQueue printQueue = new PrintQueue(); int threadCount = 3; Thread thread[] = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
thread[i] = new Thread(new Job(printQueue), "Thread" + i);
} for (int i = 0; i < threadCount; i++) {
thread[i].start();
}
}
}

输出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job during 7 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job during 5 seconds
Thread2: The document has been printed
Thread1: PrintQueue: Printing a Job during 1 seconds
Thread1: The document has been printed

从输出上看,线程0打印完成后,线程2才开始打印,然后才是线程1,没有出现一哄而上,抢占打印机的情况。这样可能没啥感觉,我们把PrintQueue如果去掉Semaphore的部分,变成下面这样:

package yjmyzz.lesson01;

public class PrintQueue {

    //private final Semaphore semaphore;

    public PrintQueue() {
//semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
} public void printJob(Object document) {
try {
//semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
}
}
}

这回的输出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread2: PrintQueue: Printing a Job during 4 seconds
Thread1: PrintQueue: Printing a Job during 8 seconds
Thread0: PrintQueue: Printing a Job during 1 seconds
Thread0: The document has been printed
Thread2: The document has been printed
Thread1: The document has been printed

可以发现,3个线程全都一拥而上,同时开始打印,也不管打印机是否空闲,实际应用中,这样必然出问题。

好的,继续,突然有一天,公司有钱了,又买了2台打印机,这样就有3台打印机了,这时候怎么办呢?简单的把PrintQueue构造器中的

    public PrintQueue() {
semaphore = new Semaphore(3);
}

就行了吗?仔细想想,就会发现问题,代码中并没有哪里能告诉线程哪个打印机正在打印,哪个打印机当前空闲,所以仍然有可能出现N个线程(N<=3)同时抢一台打印机的情况(即:如果把控制权当成钥匙的话,相当于有可能3个人各领取到了1把钥匙,但是这3把钥匙是相同的,3个人都看中了同一个箱子,都要用手中的钥匙去抢着开箱)。

所以得改进一下:

package yjmyzz.lesson02;

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class PrintQueue { private boolean freePrinters[];//用来存放打印机的状态,true表示空闲,false表示正在打印 private Lock lockPrinters;//增加了锁,保证多个线程,只能获取得锁,才能查询哪台打印机空闲的 private final Semaphore semaphore; public PrintQueue() {
int printerNum = 3;//假设有3台打印机
semaphore = new Semaphore(printerNum);
freePrinters = new boolean[printerNum]; for (int i = 0; i < printerNum; i++) {
freePrinters[i] = true;//初始化时,默认所有打印机都空闲
}
lockPrinters = new ReentrantLock();
} private int getPrinter() {
int ret = -1;
try {
lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
for (int i = 0; i < freePrinters.length; i++) {
//遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
// 并设置该打印机为繁忙状态(因为马上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后别忘记了解锁,这样后面的线程才能上来领号
lockPrinters.unlock();
}
return ret;
} public void printJob(Object document) {
try {
semaphore.acquire(); int assignedPrinter = getPrinter();//领号
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n", Thread.currentThread().getName(),
assignedPrinter, duration);
Thread.sleep(duration);
freePrinters[assignedPrinter] = true;//打印完以后,将该打印机重新恢复为空闲状态 } catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}

测试一下,这回把线程数增加到5,输出结果类似下面这样:

Thread0: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread2: The document has been printed
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread3: The document has been printed
Thread1: The document has been printed

从输出结果可以看出,一次最多只能有3个线程使用这3台打印机,而且每个线程使用的打印机互不冲突,打印完成后,空闲的打印机会给其它线程继续使用,继续折腾,如果把getPrinter()中加锁的部分去掉,即:

    private int getPrinter() {
int ret = -1;
try {
//lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
for (int i = 0; i < freePrinters.length; i++) {
//遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
// 并设置该打印机为繁忙状态(因为马上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后别忘记了解锁,这样后面的线程才能上来领号
//lockPrinters.unlock();
}
return ret;
}

再跑一下,结果如何,为了放大冲突,这回开到15个线程来抢3台打印机,输出如下:

Thread0: Going to print a job
Thread14: Going to print a job
Thread13: Going to print a job
Thread12: Going to print a job
Thread11: Going to print a job
Thread10: Going to print a job
Thread9: Going to print a job
Thread8: Going to print a job
Thread7: Going to print a job
Thread6: Going to print a job
Thread5: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
Thread0: The document has been printed
Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
Thread13: The document has been printed
Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
Thread11: The document has been printed
Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
Thread14: The document has been printed
Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread12: The document has been printed
Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
Thread10: The document has been printed
Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
Thread8: The document has been printed
Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
Thread7: The document has been printed
Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
Thread9: The document has been printed
Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
Thread5: The document has been printed
Thread6: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
Thread1: The document has been printed
Thread3: The document has been printed
Thread2: The document has been printed

注意红色的部分:Thread0与Thread14同时分配到了Printer0上了,出现了多个线程同时抢一个资源的情况。

参考文章:

http://ifeve.com/thread-synchronization-utilities-2/

http://ifeve.com/thread-synchronization-utilities-3/

http://ifeve.com/concurrency-semaphore/

java并发编程学习:用 Semaphore (信号量)控制并发资源的更多相关文章

  1. Java并发编程笔记之Semaphore信号量源码分析

    JUC 中 Semaphore 的使用与原理分析,Semaphore 也是 Java 中的一个同步器,与 CountDownLatch 和 CycleBarrier 不同在于它内部的计数器是递增的,那 ...

  2. 转:【Java并发编程】之二十三:并发新特性—信号量Semaphore(含代码)

    载请注明出处:http://blog.csdn.net/ns_code/article/details/17524153 在操作系统中,信号量是个很重要的概念,它在控制进程间的协作方面有着非常重要的作 ...

  3. 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理

    在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并 ...

  4. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  5. Java并发编程学习前期知识下篇

    Java并发编程学习前期知识下篇 通过上一篇<Java并发编程学习前期知识上篇>我们知道了在Java并发中的可见性是什么?volatile的定义以及JMM的定义.我们先来看看几个大厂真实的 ...

  6. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  7. 并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用

    (一)什么是AQS? 阅读java文档可以知道,AbstractQueuedSynchronizer是实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量.事件,等等)提供一个框架, ...

  8. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

  9. 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理

    1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...

随机推荐

  1. Linux(Centos)之安装Java JDK及注意事项

    1.准备工作 a.因为Java JDK区分32位和64位系统,所以在安装之前必须先要判断以下我们的Centos系统为多少位系统,命令如下: uname -a 解释:如果有x86_64就是64位的,没有 ...

  2. 接入统计Crash的工具Crashlytics-ios

    1. 注册账户 登录网站 https://get.fabric.io/ 来注册新的账户 2. account confirmed之后就出现相应的设置页面,设置你的team的名称 3. 出现页面提示下载 ...

  3. JavaScript基本语法(二)

    上篇博文写到JavaScript的数据类型.JavaScript包括了字符串(String).数字(Number).布尔(Boolean).数组(Array).对象(Object).空(Null).未 ...

  4. atitit.农历的公式与原理以及农历日期运算

    atitit.农历的公式与原理以及农历日期运算 1. 农历的概述1 2. 如何在电脑程序里面计算农历??1 3. 农历的公式2 4. 获取当日农历日历3 5. 历史日期公式加查表才能得到精确日期3 6 ...

  5. Android开发学习——android存储

    Android的存储 内部存储空间RAM内存:运行内存,相当于电脑的内存ROM内存:存储内存,相当于电脑的硬盘外部存储空间 SD卡:相当于电脑的移动硬盘    * 2.2之前,sd卡路径:sdcard ...

  6. Google C++单元测试框架GoogleTest---Extending Google Test by Handling Test Events

    Google TestExtending Google Test by Handling Test Events Google测试提供了一个事件侦听器API,让您接收有关测试程序进度和测试失败的通知. ...

  7. iOS流行的开源代码库

    本文介绍一些流行的iOS的开源代码库 1.AFNetworking 更新频率高的轻量级的第三方网络库,基于NSURL和NSOperation,支持iOS和OSX.https://github.com/ ...

  8. RoboGuice 3.0 (二)进阶篇

    上篇介绍了RoboGuice的接入及基本使用,其中涉及到了一个@Singleton和@ContextSingleton的注解,这些都是作用域的注解,这篇我们先说明有关作用域的问题. 一.作用域 Sco ...

  9. android studio 导入项目太慢(卡条)

    原因: 下载的项目和你当前已经下载的grandle 不一致, 导致导入的时候到网上下载相应的版本. 最简单的办法,修改你这个需要导入的项目. 需要修改的文件: 1. xxx-project/.idea ...

  10. 使用JNI封装底层input系统提供的event事件

    首先说下思路,本文采用jni技术封装底层触摸事件,封装成MotionEvent类一样,不过没有android系统MotionEvent强大.源码MotionEvent位置:java-->fram ...