【java并发核心一】Semaphore 的使用思路
最近在看一本书《Java并发编程 核心方法与框架》,打算一边学习一边把学习的经验记下来,所粘贴的代码都是我运行过的,大家一起学习,欢迎吐槽。
估计也没多少人看我的博客,哈哈,那么我还是会记下来,天空不曾留下我的痕迹,但我已飞过,而在博客园留下了我的痕迹~
1、Semaphore的初步使用
Semaphore是什么,能做什么?
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。
直接看例子吧,这个例子包含3个类,一个是线程类,一个是 Semaphore 关键代码类,一个类是主main方法类:
package com.cd.concurrent.semaphore; public class MyThread extends Thread {
private SemaphoreService service; public MyThread(String name, SemaphoreService service) {
super();
this.setName(name);
this.service = service;
} @Override
public void run() {
this.service.doSomething();
}
}
package com.cd.concurrent.semaphore; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore; public class SemaphoreService { private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private Semaphore semaphore = new Semaphore(1);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码 public void doSomething() {
try {
/**
* 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
* 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
* */
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static String getFormatTimeStr() {
return sf.format(new Date());
}
}
package com.cd.concurrent.semaphore; public class SemaphoreTest {
public static void main(String args[]) {
SemaphoreService service = new SemaphoreService();
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread("thread" + (i + 1), service);
t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了
}
}
}
运行结果:
实践证明,确实是同一个时刻只有一个线程能访问,那如果把 Semaphore 的构造方法入参改成 2 呢,修改 SemaphoreService.java 文件:
package com.cd.concurrent.semaphore; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore; public class SemaphoreService { private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码 public void doSomething() {
try {
/**
* 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
* 因为semaphore的构造方法是2,则同一时刻只允许2个线程进入,其他线程等待。
* */
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static String getFormatTimeStr() {
return sf.format(new Date());
}
}
运行SemaphoreTest,结果如下:
验证OK
2、方法 acquire( int permits ) 参数作用,及动态添加 permits 许可数量
acquire( int permits ) 中的参数是什么意思呢?可以这么理解, new Semaphore(6) 表示初始化了 6个通路, semaphore.acquire(2) 表示每次线程进入将会占用2个通路,semaphore.release(2) 运行时表示归还2个通路。没有通路,则线程就无法进入代码块。
而上面的代码中,semaphore.acquire() + semaphore.release() 在运行的时候,其实和 semaphore.acquire(1) + semaphore.release(1) 效果是一样的。
上代码:
还是3个代码,线程类没有变,用的是上面的线程类,重新写了另外两个类:
package com.cd.concurrent.semaphore; import java.util.concurrent.Semaphore; public class SemaphoreService2 extends SemaphoreService { // 之所以继承 SemaphoreService,仅仅是为了使用父类的打印时间的方法 0.0 private Semaphore semaphore = new Semaphore(6);// 6表示总共有6个通路 public void doSomething() {
try {
semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
semaphore.release(2); // 释放占用的 2 个通路
} catch (InterruptedException e) {
e.printStackTrace();
}
} public int availablePermits() { // 查看可用通路数
return semaphore.availablePermits();
}
}
package com.cd.concurrent.semaphore; public class SemaphoreTest2 {
public static void main(String args[]) {
SemaphoreService2 service = new SemaphoreService2(); // 使用总 6 通路,每个线程占用2通路
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread("thread" + (i + 1), service);
t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了
System.out.println("可用通路数:" + service.availablePermits());
}
}
}
运行结果:
如果 acquire 的数量大于 release 的数量,则 通路迟早会被使用完,如果线程比较多,得不到后续运行,出现线程堆积内存,最终java进程崩掉;如果 acquire 的数量小于 release 的数量,就会出现并发执行的线程越来越多(换句话说,处理越来越快),最终也有可能出现问题。
比如,象上面的代码,SemaphoreService2.java 中 semaphore.release(2) 如果改成 semaphore.release(1) 则 就会出现有5个线程得不到运行堆积的情况,可以算一下:6-2-2-2+1+1+1=3,运行完一个回合后,还剩3个通路,3-2+1,第二回合,还剩2个通路,2-2+1=1,第3个回合,还剩一个通路,不足以运行任何一个线程。
把上面说的用代码实现一下,修改 SemaphoreService2.java 如下:
package com.cd.concurrent.semaphore; import java.util.concurrent.Semaphore; public class SemaphoreService2 extends SemaphoreService { // 之所以继承 SemaphoreService,仅仅是为了使用父类的打印时间的方法 0.0 private Semaphore semaphore = new Semaphore(6);// 6表示总共有6个通路 public void doSomething() {
try {
semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
semaphore.release(1); // 释放占用的 1 个通路
} catch (InterruptedException e) {
e.printStackTrace();
}
} public int availablePermits() {
return semaphore.availablePermits();
}
}
运行 SemaphoreTest2 结果:
3、acquire 的不可中断实现
仔细看一下上面的代码,semaphore.acquire() 和 semaphore.acquire(int permits) 是会抛出异常 InterruptedException 的,如果在 acquire 和 release 之间的代码是一个比较慢和复制的运算,如内存占用过多,或者栈深度很深等,jvm会中断这块代码。
如何才能不让 jvm 中断 代码执行呢?
答案是:使用 acquireUninterruptibly() 替换acquire()、使用 acquireUninterruptibly(int permits) 替换 acquire(int permits) 。
acquireUninterruptibly 不会抛出 InterruptedException ,一个代码块一时执行不完,还会继续等待执行。
个人觉得,不要随便使用 acquireUninterruptibly ,因为 jvm 中断执行,是自身的一种自我保护机制,保证 java 进程的正常,除了特殊情况必须用 acquireUninterruptibly 外,都应该 使用 acquire ,同时,改进一下 SemaphoreService2 的 doSomething 方法,将 release 放到 finally 块 中,如下。
public void doSomething() {
try {
semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(2); // release 放到 finally 中
}
}
4、其他一些常有工具方法
availablePermits() 方法在前面用过,表示返回 Semaphore 对象中的当前可用许可数,此方法通常用于调试,因为许可数量(通路)可能是实时在改变的。
drainPermits() 方法可获取并返回立即可用的所有许可(通路)个数,并将可用许可置为0。
getQueueLength() 获取等待许可的线程个数。
hasQueuedThreads() 判断有没有线程在等待这个许可。
getQueueLength() 和 hasQueuedThreads() 都是在判断当前有没有等待许可的线程信息时使用。
这里就不写代码校验了,你们可以在 SemaphoreService 或者 SemaphoreService2 中加入这个信息试一下。
5、线程公平性
上面用的 Semaphore 构造方法是 Semaphore semaphore = new Semaphore(int permits)
其实,还有一个构造方法: Semaphore semaphore = new Semaphore(int permits , boolean isFair)
isFair 的意思就是,是否公平,获得锁的顺序与线程启动顺序有关,就是公平,先启动的线程,先获得锁。isFair 不能100% 保证公平,只能是大概率公平。
isFair 为 true,则表示公平,先启动的线程先获得锁。
6、方法 tryAcquire() 、 tryAcquire(int permits)、 tryAcquire(int permits , long timeout , TimeUint unit) 的使用:
tryAcquire 方法,是 acquire 的扩展版,tryAcquire 作用是尝试得获取通路,如果未传参数,就是尝试获取一个通路,如果传了参数,就是尝试获取 permits 个 通路 、在指定时间 timeout 内 尝试 获取 permits 个通路。
上代码试试看:
3个类,线程类未变,以下是修改了的两个类:
package com.cd.concurrent.semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; public class SemaphoreService3 extends SemaphoreService { // 之所以继承 SemaphoreService,仅仅是为了使用父类的打印时间的方法 0.0 private Semaphore semaphore = new Semaphore(6, true);// 6表示总共有6个通路,true 表示公平 public void doSomething() {
try {
if (semaphore.tryAcquire(2, 3, TimeUnit.SECONDS)) { // 在 3秒 内 尝试获取 2 个通路 System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr()
+ ",当前是否有进程等待:" + semaphore.hasQueuedThreads() + ",等待进程数:" + semaphore.getQueueLength());
semaphore.release(2); // 释放占用的 2 个通路
} else {
System.out.println(Thread.currentThread().getName() + ":doSomething 没有获取到锁-准备退出-" + getFormatTimeStr());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} public int availablePermits() {
return semaphore.availablePermits();
}
}
package com.cd.concurrent.semaphore; public class SemaphoreTest3 {
public static void main(String args[]) {
SemaphoreService3 service = new SemaphoreService3(); // 使用总 6 通路,每个线程占用2通路,尝试获取锁
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread("thread" + (i + 1), service);
t.start();
}
}
}
SemaphoreTest3 运行结果:
7、多进路-多处理 vs 多进路-单处理
在上面的代码中,我们之所以可以实现单处理,是因为在上面的所有线程都共有了同一个 Semaphore 来进行进程处理,那么如果 Semaphore 本身就是进程的一部分呢,会怎么样呢?
比如,修改 第一个例子中的 SemaphoreTest 如下:
package com.cd.concurrent.semaphore; public class SemaphoreTest {
public static void main(String args[]) {
for (int i = 0; i < 10; i++) {
SemaphoreService service = new SemaphoreService();
MyThread t = new MyThread("thread" + (i + 1), service);
t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了
}
}
}
运行 SemaphoreTest 结果:
所有线程同时执行了。
如果 SemaphoreTest 类不进行修改,如何实现第一个例子 中的 单处理呢?
也简单,修改 SemaphoreService ,代码如下:
package com.cd.concurrent.semaphore; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore; public class SemaphoreService { private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码 public void doSomething() {
try {
/**
* 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
* 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
* */
semaphore.acquire(); doSomethingMain(); // 将主要处理部分封装成一个方法 semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
} private static synchronized void doSomethingMain() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
} public static String getFormatTimeStr() {
return sf.format(new Date());
}
}
注意:doSomethingMain() 方法必须是 static synchronized 的才行,因为 多线程调用的话,static 方法是类方法,这样 synchronized 同步 才能针对整个类同步,否则 就只能针对单线程多个地方调用同步。
修改 SemaphoreService ,运行 SemaphoreTest 结果:
运行达到想要的效果。
这里,抛出一个问题,上面的代码,不用 synchronized 实现,而使用 ReentrantLock 来实现,按理说会更好的,原因如下:
synchronized 是 jvm 层面的实现,ReentrantLock 是 jdk 层面的实现,synchronized 的缺点如下:
1)不能响应中断;
2)同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待
3)锁的释放由虚拟机来完成,不用人工干预,不过此即使缺点也是优点,优点是不用担心会造成死锁,缺点是由可能获取到锁的线程阻塞之后其他线程会一直等待,性能不高。
而lock接口的提出就是为了完善synchronized的不完美的,首先lock是基于jdk层面实现的接口,和虚拟机层面不是一个概念;其次对于lock对象中的多个方法的调用,可以灵活控制对共享资源变量的操作,不管是读操作还是写操作
那么上面的代码如果使用 ReentrantLock 来实现,岂不是更好吗?好,修改 SemaphoreService:
package com.cd.concurrent.semaphore; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock; public class SemaphoreService { private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码 private ReentrantLock lock = new ReentrantLock(); public void doSomething() {
try {
/**
* 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
* 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
* */
semaphore.acquire(); lock.lock();
doSomethingMain(); // 将主要处理部分封装成一个方法 semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} private void doSomethingMain() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
} public static String getFormatTimeStr() {
return sf.format(new Date());
}
}
运行 SemaphoreTest 结果:
和预期的不一样呀,10个线程基本是同时执行了,那么问题出在哪里呢?
答案会公布到评论里哦~
【java并发核心一】Semaphore 的使用思路的更多相关文章
- 【Java并发核心三】CountDownLatch、CyclicBarrier及Phaser
个人感觉,看书学习还是需要“不求甚解”,因为一旦太过于计较小的得失,就容易钻牛角尖,学习进度也慢.我们完全可以先学一个大概,等到真正用到的时候再把那些细节丰富起来,就更有针对性. 所以,针对java并 ...
- Java 并发专题 : Semaphore 实现 互斥 与 连接池
继续并发方面的知识.今天介绍Semaphore,同样在java.util.concurrent包下. 本来准备通过例子,从自己实现到最后使用并发工具实现,但是貌似效果并不是很好,有点太啰嗦的感觉,所有 ...
- java并发编程之一--Semaphore的使用
1.介绍 Semaphore 中文的含义 信号,信号系统,此类的只要作用就是限制线程的并发的数量. Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程 ...
- JAVA并发框架之Semaphore实现生产者与消费者模型
分类: Java技术 锁和信号量(Semaphore)是实现多线程同步的两种常用的手段.信号量需要初始化一个许可值,许可值可以大于0,也可以小于0,也可以等于0. 如果大于0,表示 ...
- Java并发工具类Semaphore应用实例
package com.thread.test.thread; import java.util.Random; import java.util.concurrent.*; /** * Semaph ...
- Java并发编程:Semaphore、CountDownLatch、CyclicBarrier
首先我们来实现一个功能:当我们启动一个系统的时候需要初始化许多数据,这时候我们可能需要启动很多线程来进行数据的初始化,只有这些系统初始化结束之后才能够启动系统.其实在Java的类库中已经提供了Sema ...
- Java并发系列[6]----Semaphore源码分析
Semaphore(信号量)是JUC包中比较常用到的一个类,它是AQS共享模式的一个应用,可以允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制.Semapho ...
- 【Java并发核心九】并发集合框架
1.List接口:ArrayList 和 Vector ArrayList不是线程安全的,Vector是线程安全的,Vector有一个子类,可实现后进先出(LIFO)的对象堆栈(LinkedList ...
- 【java并发核心八】Fork-Join分治编程
jdk1.7中提供了Fork/Join并行执行任务框架,主要作用就是把大任务分割成若干个小任务,再对每个小任务得到的结果进行汇总. 正常情况下,一些小任务我们可以使用单线程递归来实现,但是如果要想充分 ...
随机推荐
- DSO windowed optimization 代码 (2)
3 非 Schur Complement 部分信息计算 参考<DSO windowed optimization 公式>,非Schur Complement 部分指 \(H_{XX}\) ...
- JS的call方法的作用解释,简单易懂
先看看关于call()的官方解释,“调用一个对象的一个方法,以另一个对象替换当前对象.”,看了这样的解释,或许让你更摸不着头脑了.看例子: var x = "我是全局变量"; // ...
- jQuery中【width(),innerWidth(),outerWidth()】
这个问题,已经别扭我多年了,今天终于彻底解决了,拿出来庆贺一下.jquery作为开源项目,无论从思路上,还是从严谨性上,让人崇敬. 随着时间的流逝,jquery的一些功能被逐渐挖掘出来.通过jQuer ...
- 存储器结构、cache、DMA架构分析--【原创】
存储器的层次结构 高速缓冲存储器 cache 读cache操作 cache如果包含数据就直接从cache中读出来,因为cache速度要比内存快 如果没有包含的话,就从内存中找 ...
- sga 操作命令
用SYS用户以SYSDBA身份登录系统alter system set sga_max_size=2000m scope=spfile;alter system set sga_target=2000 ...
- elasticsearch5.0集群大数据量迁移方法及注意事项
当es集群的数据量较小的情况下elasticdump这个工具比较方便,但是当数据量达到一定级别比如上百G的时候,elasticdump速度就很慢了,此时我们可以使用快照的方法进行备份 elasticd ...
- Linux服务器性能评估
一.影响Linux服务器性能的因素 1. 操作系统级 CPU 内存 磁盘I/O带宽 网络I/O带宽 2. 程序应用级 二.系统性能评估标准 影响性能因素 影响性能因素 评判标准 好 坏 糟糕 CPU ...
- android 知识点汇总
1.activity 它是 android 应用程序的基本功能单元.一个Activity是一个应用程序组件,提供一个屏幕,用户可以用来交互为了完成某项任务,例如拨号.拍照.Activity 本身是没有 ...
- js----DOM对象(事件)
节点操作: 创建节点:var ele_a = document.createElement('a'); 添加节点:ele_parent.appendChild(ele_img); 删除节点:ele_p ...
- 【C++ Primer | 15】虚继承
虚基类 一.虚基类介绍 多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次.如下图所示: 类A派生出类B和 ...