1.4 J.U.C工具类
CountDownLatch
CountDownLatch
:用于监听某些初始化操作,并且线程进行阻塞,等初始化执行完毕后,通知主线程继续工作执行。
public class UseCountDownLatch {
public static void main(String[] args) {
// 构造参数表示唤醒线程需要通知的次数
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.err.println("进入t1线程..");
try {
Thread.sleep(3000); //做了一些初始化的准备..
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("t1线程初始化完毕, 通知t3 线程继续操作");
// 通知唤醒
countDownLatch.countDown();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.err.println("进入t2线程..");
try {
Thread.sleep(4000); //做了一些初始化的准备..
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("t2线程初始化完毕, 通知t3 线程继续操作");
// 通知唤醒
countDownLatch.countDown();
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.err.println("进入t3线程.., 并且进入等待...");
try {
// 休眠,等待唤醒
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("t3线程 进行后续的操作!");
}
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
//运行结果
进入t1线程..
进入t2线程..
进入t3线程.., 并且进入等待...
t1线程初始化完毕, 通知t3 线程继续操作
t2线程初始化完毕, 通知t3 线程继续操作
t3线程 进行后续的操作!
t3线程进入之后,调用countDownLatch.await();方法来等待其他线程完成初始化之后再开始操作。
CyclicBarrier
CyclicBarrier
:栅栏的概念,多线程的进行阻塞,等待某一个临界值条件满足后,同时执行!
假设一个场景:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待!
public class UseCyclicBarrier {
static class Runner implements Runnable {
private String name;
private CyclicBarrier cyclicBarrier;
public Runner(String name, CyclicBarrier cyclicBarrier){
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.err.println("运动员:" + this.name + ", 进行准备工作!");
// 随机休眠
Thread.sleep(1000 * (new Random()).nextInt(10));
System.err.println("运动员:" + this.name + ", 准备OK!");
// 阻塞等待
this.cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.err.println("运动员: " + this.name + ", GO! GO! GO!");
}
}
public static void main(String[] args) {
// 栅栏阻塞三个线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
// 使用线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(new Thread(new Runner("张三", cyclicBarrier)));
executorService.submit(new Thread(new Runner("李四", cyclicBarrier)));
executorService.submit(new Thread(new Runner("王五", cyclicBarrier)));
executorService.shutdown();
}
}
// 运行结果
运动员:李四, 进行准备工作!
运动员:张三, 进行准备工作!
运动员:王五, 进行准备工作!
运动员:张三, 准备OK!
运动员:王五, 准备OK!
运动员:李四, 准备OK!
运动员: 李四, GO! GO! GO!
运动员: 张三, GO! GO! GO!
运动员: 王五, GO! GO! GO!
每个人都要等待其他人准备好之后再一起起跑
实际场景使用
图中,方法4需要等待方法1,2,3运行的结果作为参数来运行,因此方法4需要等待其余方法执行完成才能运行。与使用监听器监听其他方法的执行结果相比,使用CyclicBarrier
来进行阻塞更为直观方便。
Future与Caller回调
- Future模式,也是非常经典的设计模式,这种模式主要就利用空间换时间的概念,也就是说异步执行(需要开启一个新的线程)。
- 在互联网高并发的应用服务中,我们随处可见这种理念和代码,主要就是使用了这种模式!
- Future模式非常适合在处理很耗时很长的业务逻辑时进行使用,可以有效的减小系统的响应时间,提高系统的吞吐量!
public class UseFuture implements Callable<String> {
private String param;
public UseFuture(String param){
this.param = param;
}
@Override
public String call() throws Exception {
//模拟执行业务逻辑的耗时
Thread.sleep(3000);
String result = this.param + ", 处理完成!";
return result;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
String queryStr1 = "query1";
String queryStr2 = "query2";
FutureTask<String> future1 = new FutureTask<String>(new UseFuture(queryStr1));
FutureTask<String> future2 = new FutureTask<String>(new UseFuture(queryStr2));
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(future1); //异步的操作
executorService.submit(future2); //异步的操作
System.err.println("处理其他相关的任务...");
Thread.sleep(2000);
String ret1 = future1.get();
String ret2 = future2.get();
System.err.println("数据处理完成: " + ret1);
System.err.println("数据处理完成: " + ret2);
}
}
// 运行结果
处理其他相关的任务...
数据处理完成: query1, 处理完成!
数据处理完成: query2, 处理完成!
future1 和 future2与主线程为异步操作
利用设计模式模拟Future
Future模式有点类似于商品订单。
比如在网购时,当看重某一件商品事,就可以提交订单,当订单处理完成后,在家里等待商品送货上门即可。
或者说更形象的我们发送Ajax请求的时候,页面是异步的进行后台处理,用户无须一直等待请求的结果,可以继续浏览或操作其他内容。
此处将模拟Future模式
先定义接口Data
,然后分别创建FutureData
包装类和RealData
真实数据类,两个类均实现Data
接口
然后创建FutureClient
客户端,最后创建Main
来进行测试
/**
* Data 接口
*/
public interface Data {
String getRequest();
}
/**
* Data 包装类
*/
public class FutureData implements Data {
// 真实数据对象的引用
private RealData realData;
// 实际处理的状态
private boolean isReady = false;
public synchronized void setRealData(RealData realData) {
if(isReady) {
return;
}
//如果真实对象赋值成功, 那么就认为数据已经准备好了
this.realData = realData;
isReady = true;
//真实数据已经准备好了的时候 我们进行唤醒操作
notify();
}
@Override
public synchronized String getRequest() {
while(!isReady) {
try {
// 等待线程准备真实数据
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 返回真实数据
return this.realData.getRequest();
}
}
/**
* Data 真实数据类
*/
public class RealData implements Data {
private String result;
/**
* 创建
* @param queryStr
*/
public RealData(String queryStr) {
System.err.println("根据查询参数:" + queryStr + "进行查询数据库操作, 这可能需要5秒左右...");
try {
//查询耗时
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = "100条数据";
}
@Override
public String getRequest() {
return result;
}
}
/**
* Future 客户端
*/
public class FutureClient {
public Data request(final String queryStr) {
FutureData futureData = new FutureData();
//异步的起一个线程去进行相应的处理
new Thread(new Runnable() {
@Override
public void run() {
//需要把请求参数 设置到真实的数据处理对象中去
RealData realData = new RealData(queryStr);
//真实请求处理完成以后, 我们要进行设置结果给包装类对象
futureData.setRealData(realData);
}
}).start();
return futureData;
}
}
public class Main {
public static void main(String[] args) {
FutureClient fc = new FutureClient();
Data data = fc.request("请求参数.."); //异步执行的
System.err.println("做其他的相关业务操作!");
String ret = data.getRequest(); //才是真正的获取实际数据的方法
System.err.println("--------: " + ret);
}
}
// 运行结果
做其他的相关业务操作!
根据查询参数:请求参数..进行查询数据库操作, 这可能需要5秒左右...
--------: 100条数据 //5秒后才输出该信息
Exchanger多线程间数据交换
Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据;
两个线程通过exchange方法交换数据,如果一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange方法
当两个线程都达到同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方
使用场景:
遗传算法:遗传算法里需要选出两个人作为交换对象,这时会交换两人的数据,并使用交叉规则得出两个人交换结果。
用于校对工作:A、B同时录入数据,然后对A、B进行比较,看是否录入一致,保证数据正确性。
比如我们需要将纸制银流通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对这两个Excel数据进行校对,看看是否录入的一致。
public class UseExchanger {
private static final Exchanger<String> exchanger = new Exchanger<>();
private static ExecutorService executorService = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
executorService.execute(new Runnable() {
@Override
public void run() {
String A = "银行流水A";
try {
String B = exchanger.exchange(A); //交换我自己的数据 并且获取别人的数据
System.err.println("线程A: " + B);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
String B = "银行流水B";
try {
String A = exchanger.exchange(B); //交换我自己的数据 并且获取别人的数据
System.err.println("线程B: " + A);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executorService.shutdown();
}
}
ForkJoin并行
Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
Fork/Join中两个重要的类:
ForkJoinTask
:使用该框架,需要创建一个ForkJoin
任务,它提供在任务中执行fork和join操作的机制。一般情况下,我们并不需要直接继承ForkJoinTask类,只需要继承它的子类,它的子类有两个:RecursiveAction
:用于没有返回结果的任务。RecursiveTask
:用于有返回结果的任务。
ForkJoinPool
:任务ForkJoinTask
需要通过ForkJoinPool
来执行。
public class UseForkJoin extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2; //阈值
private int start;
private int end;
public UseForkJoin(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0; // 1 + 2 .... + 100
boolean canCompute = (end - start) <= THRESHOLD;
if(canCompute) {
for(int i = start; i <=end; i++ ){
sum += i;
}
} else {
//如果说 任务数大于阈值的话, 就进行拆分 fork操作 然后去join
// 1 + 100 /2 = 50
int middle = (start + end)/2;
UseForkJoin leftTask = new UseForkJoin(start, middle);
UseForkJoin rightTask = new UseForkJoin(middle+1, end);
//执行左右两边的任务
leftTask.fork();
rightTask.fork();
//等待任务执行完成后 进行获取结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
UseForkJoin ufj = new UseForkJoin(1, 100);
Future<Integer> result = pool.submit(ufj);
System.err.println("最终执行结果为:" + result.get());
}
}
// 运行结果:
最终执行结果为:5050
Master-Worker并发组件模拟-生产消费者模式
Master-Worker模式是常用的并行计算模式。
它的核心思想是系统由两类进程协作工作:Master进程和Worker进程。
Master负责接收和分配任务,Worker负责处理子任务。
当各个Worker子进程处理完成后,会将结果返回给Master,由Master做归纳和总结。
其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。
设置一个Master类和一个Worker类,然后定义一个Task任务类,最后编写测试Main
public class Master {
//1 承装任务的一个容器:
private ConcurrentLinkedQueue<Task> taskQueue = new ConcurrentLinkedQueue<>();
//2 承装worker执行器
private HashMap<String, Thread> workers = new HashMap<>();
//3 接受worker处理成功的结果集合
private ConcurrentHashMap<String, Object> resultMap = new ConcurrentHashMap<>();
//4 构造方法里面,要对worker进行一个初始化操作
public Master(Worker worker, int workCount) {
//4.1 每一个worker 应该有master里 任务队列容器的引用
worker.setTaskQueue(this.taskQueue);
//4.2 每一个worker 应该有master里 结果集容器的引用
worker.setResultMap(this.resultMap);
//4.3 我们把所有的worker进行初始化 放入 workers容器中
for(int i = 0; i < workCount; i ++){
this.workers.put(Integer.toString(i), new Thread(worker));
}
}
//5 需要一个提交任务的方法
public void submit(Task task) {
this.taskQueue.add(task);
}
//6 需要有一个真正让我们Master里的所有Worker进行工作的方法
public void execute() {
for(Map.Entry<String, Thread> me : this.workers.entrySet()) {
me.getValue().start();
}
}
//7 需要有一个统计的方法,用于合并结果集
public int getResult() {
int sum = 0;
for(Map.Entry<String, Object> me : resultMap.entrySet()) {
sum += (Integer)me.getValue();
}
return sum;
}
//8 判断是否所有的worker都完成工作了 如果完成返回true
public boolean isComplete() {
for(Map.Entry<String, Thread> me : this.workers.entrySet()) {
if(me.getValue().getState() != Thread.State.TERMINATED){
return false;
}
}
return true;
}
}
public class Worker implements Runnable {
private ConcurrentLinkedQueue<Task> taskQueue;
private ConcurrentHashMap<String, Object> resultMap;
public void setTaskQueue(ConcurrentLinkedQueue<Task> taskQueue) {
this.taskQueue = taskQueue;
}
public void setResultMap(ConcurrentHashMap<String, Object> resultMap) {
this.resultMap = resultMap;
}
@Override
public void run() {
while(true) {
Task task = this.taskQueue.poll();
if(task == null) break;
try {
Object result = handle(task);
this.resultMap.put(Integer.toString(task.getId()), result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private Random r = new Random();
//实际做每一个工作!
private Object handle(Task task) throws Exception {
//每一个任务处理的时间是:
//Thread.sleep(200 * r.nextInt(10));
Thread.sleep(200);
int ret = task.getCount();
return ret;
}
}
public class Task {
private int id;
private int count;
public Task() {
}
public Task(int id, int count) {
this.id = id;
this.count = count;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
public class Main {
public static void main(String[] args) {
System.err.println("线程数:" + Runtime.getRuntime().availableProcessors());
Master master = new Master(new Worker(), Runtime.getRuntime().availableProcessors());
Random r = new Random();
for(int i = 0; i < 100; i ++) {
Task t = new Task(i, r.nextInt(1000));
master.submit(t);
}
master.execute();
long start = System.currentTimeMillis();
//CountDownLatch
while(true) {
if(master.isComplete()) {
long end = System.currentTimeMillis();
int result = master.getResult();
System.err.println("最终执行结果: " + result + ", 总耗时: " + (end - start));
break;
}
}
}
}
// 运行结果:
线程数:8
最终执行结果: 56007, 总耗时: 2613
Semaphore信号量与限流策略
Semaphore信号量非常适合高并发访问限制
相关概念:
-
PV(page view) 网站的总访问量,页面浏览量或点击量,用户每刷新一次就会被记录一次。
-
UV(unique Visitor) 访问网站的一台电脑客户端为一个访客。一般来讲时间上以00:00-24:00之内相同ip的客户端只记录。
-
QPS(query per second) 即每秒查询数,qps很大程度上代表了系统业务上的繁忙程度,每次请求的背后,可能对应着多次磁盘I/O,多次网络请求,多个cpu时间片等。我们通过qps可以非常直观的了解当前系统业务情况,一旦当前qps超过所设定的预警阀值,可以考虑增加机器对集群扩容,以免压力过大导致宕机,可以根据前期的压力测试得到估值,在结合后期综合运维情况,估算出阀值。
-
RT(response time) 请求的响应时间,这个指标非常关键,直接说明前端用户的体验,任何系统设计师都想降低rt时间。当然还涉及cpu、内存、网络、磁盘等情况,更细节的问题很多,如select、update、delete/ps等数据库层面的统计。
-
容量评估:一般来说通过开发、运维、测试、以及业务等相关人员,综合出系统的一系列阀值,然后我们根据关键阀值如qps、rt等,对系统进行有效的变更。
一般来讲,我们进行多轮压力测试以后,可以对系统进行峰值评估,采用所谓的80/20原则,即80%的访问请求将在20%的时间内达到。这样我们可以根据系统对应的PV计算出峰值qps。
峰值qps= (总PV × 80%)/ (60 × 60 × 24 × 20%)
然后在将总的峰值qps除以单台机器所能承受的最高的qps值,就是所需要机器的数量:机器数 = 总的峰值qps / 压测得出的单机极限qps
public class UseSemaphore { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(20); // 最多只能并发执行5个线程 Semaphore semaphore = new Semaphore(5); for(int index = 1; index <= 20; index ++){ final int token = index; Runnable run = new Runnable() { @Override public void run() { try { semaphore.acquire(); //进行相关的业务操作 System.err.println("获得许可,执行操作..." + token); long sleepTime = (long)(Math.random() * 10000); Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } } }; executorService.execute(run); } System.err.println("queue length: " + semaphore.getQueueLength()); executorService.shutdown(); } } // 运行结果 获得许可,执行操作...1 获得许可,执行操作...5 获得许可,执行操作...3 获得许可,执行操作...4 获得许可,执行操作...2 queue length: 13 //达到了限制,阻塞了13个线程 获得许可,执行操作...6 获得许可,执行操作...7 获得许可,执行操作...8 获得许可,执行操作...10 获得许可,执行操作...11 获得许可,执行操作...12 获得许可,执行操作...9 获得许可,执行操作...13 获得许可,执行操作...14 获得许可,执行操作...15 获得许可,执行操作...16 获得许可,执行操作...17 获得许可,执行操作...18 获得许可,执行操作...19 获得许可,执行操作...20