前言
前段时候学习单例模式的时候,有用到多线程并发数去测试单例模式的线程安全。但是当时时间比较紧没有进行记录,今天特地记录一下。
1、先看代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:20
* @description :
*/
public class ConcurrentExecutor {
/**
* @param runHandler
* @param executeCount 发起请求总数
* @param concurrentCount 同时并发执行的线程数
* @throws Exception
*/
public static void execute(final RunHandler runHandler, int executeCount, int concurrentCount) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
//控制信号量,此处用于控制并发的线程数
final Semaphore semaphore = new Semaphore(concurrentCount);
//闭锁,可实现计数量递减
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
for (int i = 0; i < executeCount; i++) {
executorService.execute(new Runnable() {
public void run() {
try {
//执行此方法用于获取执行许可,当总计未释放的许可数不超过executeCount时,
//则允许同性,否则线程阻塞等待,知道获取到许可
semaphore.acquire();
runHandler.handler();//回调函数
//释放许可
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();//线程阻塞,知道闭锁值为0时,阻塞才释放,继续往下执行
executorService.shutdown();
}
public interface RunHandler {
void handler();
}
}
2、Executors
java通过Executors可以创建四种线程池:
- newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。超出的线程会在队列中等待
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
第一种:newCachedThreadPool
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
第二种:newFixedThreadPool
因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
第三种:newScheduledThreadPool
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
第四种:newSingleThreadExecutor
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
3、ExecutorService
ExecutorService的几个方法:
1、execute(Runnable) 这个方法接收一个Runnable实例,并且异步的执行。 这个方法有个问题,就是没有办法获知task的执行结果。如果我们想获得task的执行结果,我们可以传入一个Callable的实例。
2、submit(Runnable) submit(Runnable)和execute(Runnable)区别是前者可以返回一个Future对象,通过返回的Future对象,我们可以检查提交的任务是否执行完毕。如果任务执行完成,future.get()方法会返回一个null。注意,future.get()方法会产生阻塞。
3、submit(Callable) submit(Callable)和submit(Runnable)类似,也会返回一个Future对象,但是除此之外,submit(Callable)接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。如果任务执行完成,future.get()方法会返回Callable任务的执行结果。注意,future.get()方法会产生阻塞。
4、invokeAny(…) 这个方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个。
5、invokeAll(…) invokeAll(…)与 invokeAny(…)类似也是接收一个Callable集合,但是前者执行之后会返回一个Future的List,其中对应着每个Callable任务执行后的Future对象。
ExecutorService的关闭:
1、shutdown() :停止接收新任务,原来的任务继续执行
- 停止接收新的submit的任务;
- 已经提交的任务(包括正在跑的和队列中等待的),会继续执行完成;
- 等到第2步完成后,才真正停止;
2、shutdownNow():停止接收新任务,原来的任务停止执行
- 跟 shutdown() 一样,先停止接收新submit的任务;
- 忽略队列里等待的任务;
- 尝试将正在执行的任务interrupt中断;
- 返回未执行的任务列表;
3、awaitTermination(long timeOut, TimeUnit unit):当前线程阻塞 timeout 和 TimeUnit 两个参数,用于设定超时的时间及单位
当前线程阻塞,直到:
- 等所有已提交的任务(包括正在跑的和队列中等待的)执行完;
- 或者 等超时时间到了(timeout 和 TimeUnit设定的时间);
- 或者 线程被中断,抛出InterruptedException;
然后会监测 ExecutorService 是否已经关闭,返回true(shutdown请求后所有任务执行完毕)或false(已超时)
4、shutdown() 和 shutdownNow() 的区别
shutdown() 只是关闭了提交通道,用submit()是无效的;而内部该怎么跑还是怎么跑,跑完再停。
shutdownNow() 能立即停止线程池,正在跑的和正在等待的任务都停下了。
5、shutdown() 和 awaitTermination() 的区别
shutdown() 后,不能再提交新的任务进去;但是 awaitTermination() 后,可以继续提交。
awaitTermination() 是阻塞的,返回结果是线程池是否已停止(true/false);shutdown() 不阻塞。
6、总结
1、优雅的关闭,用 shutdown()
2、想立马关闭,并得到未执行任务列表,用shutdownNow()
3、优雅的关闭,并允许关闭声明后新任务能提交,用 awaitTermination()
4、关闭功能 【从强到弱】 依次是:shuntdownNow() > shutdown() > awaitTermination()
4、Semaphore
Semaphore是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。
Semaphore 初始化必须提供并发线程数,因为Semaphore 并没有提供更新并发数量的函数。
Semaphore主要方法:
1、类Semaphore的构造函数permits 是许可的意思,代表同一时间,最多允许permits个线程执行acquire() 和release()之间的代码。
2、方法acquire() 的功能是每调用1次此方法,就消耗掉1个许可。
3、方法acquire(n) 的功能是每调用1次此方法,就消耗掉n个许可。
4、方法release() 的功能是每调用1次此方法,就动态添加1个许可。
5、方法release(n) 的功能是每调用1次此方法,就动态添加n个许可。
6、方法acquireUnnterruptibly() 作用是是等待进入acquire() 方法的线程不允许被中断。
7、方法availablePermits() 返回Semaphore对象中当前可以用的许可数。
8、方法drainPermits() 获取并返回所有的许可个数,并且将可用的许可重置为0
9、方法 getQueueLength() 的作用是取得等待的许可的线程个数
10、方法 hasQueueThreads() 的作用是判断有没有线程在等待这个许可
11、公平和非公平信号量:
有些时候获取许可的的顺序与线程启动的顺序有关,这是的信号量就要分为公平和非公平的。所谓的公平信号量是获得锁的顺序与线程启动的顺序有关,但不代表100%获得信号量,仅仅是在概率上能保证,而非公平信号量就是无关的。
例如:
Semaphore semaphore = new Semaphore(1,false);
False:表示非公平信号量,即线程启动的顺序与调用semaphore.acquire() 的顺序无关,也就是线程先启动了并不代表先获得 许可。
True:公平信号量,即线程启动的顺序与调用semaphore.acquire() 的顺序有关,也就是先启动的线程优先获得许可。
12、方法tryAcquire() 的作用是尝试获取1个许可。如果获取不到则返回false,通常与if语句结合使用,其具有无阻塞的特点。无阻塞的特点可以使不至于在同步处于一直持续等待的状态。
13、方法tryAcquire(n) 的作用是尝试获取n个许可,如果获取不到则返回false
14、方法tryAcquire(long timeout,TimeUnit unit) 的作用是在指定的时间内尝试获取1个许可,如果获取不到则返回false
15、方法tryAcquire(int permits,long timeout,TimeUnit unit) 的作用是在指定的时间内尝试获取n 个许可,如果获取不到则返回false
5、 CountDownLatch
CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
CountDownLatch 只有一个构造方法,需要传入一个int类型的参数,这个数值一般是你的线程数
CountDownLatch 类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };