前言
1024程序员节. 今天写点干货.
在Java的多线程内, 有非常多的组成和变量. 初学时, 经常拿捏不住. 下面我们带入生活的场景, 简单的描述下Java中的各种使用场景.
多线程的基本组成 Thread (多人相同工作)
在日常的生活中. 我们经常需要做一件事情. 我们认为时一个Task. 那么一个Task有时可以分成多个相同的Task, 以及多个不同的Task.
比如: 饺子店出售饺子
- 场景1-1: 使用的是速冻饺子. 店家只需要负责下饺子就可以了. 那么当客人比较多的时候. 一口锅自然是不够的, 我们需要准备5口锅进行下饺子.
那么, 你可以认为是有5个工人同时进行下饺子这个操作. 图示:
此时, 我们可以写出这样的伪代码.
// 下饺子
Task cookDumpling.
// 准备5个工人进行下饺子
Thread worker1 = new Thread(cookDumpling);
Thread worker2 = new Thread(cookDumpling);
Thread worker3 = new Thread(cookDumpling);
Thread worker4 = new Thread(cookDumpling);
Thread worker5 = new Thread(cookDumpling);
// 发号司令 5个人开始干活
worker1.start();
worker2.start();
worker3.start();
worker4.start();
worker5.start();
- 场景1-2: 当然, 我们并不希望. 这5个工人直接下完一次饺子. 就可以停止工作. (老资本家了.) 所以, 我们希望他们循环工作. 并且没有工作的时候, 也不要停止工作, 而是进行等待. 所以, 我们可以将工人的task和run方法分离. 可以写成这样的伪代码.
// 工作池. 所有的需求不断向工作池中放即可.
List<Task> queueTaskList = new Queue();
// 任务类
class Task{
// 任务的Id
int id;
// 订单是啥 - 比如这个客人订了2份鲜肉水饺. 2份韭菜水饺.
Order order;
}
// 工作类
class Worker{
public void work(){
// 查看当前时间是否是工作时间. 如果是工作时间. 就给我不停的干活.
while(currentTime>startTime & currentTime<endTime){
if(queueTaskList.isNotEmpty()){
// 弹出最前的需要执行的任务
var task = queueTaskList.pop();
// 执行这个任务
task.perform();
}else{
// 否则过1分钟过后. 再来查看工作池中是否有工作需要干.
}
}
}
}
- 场景1-3: 有时, 我们服务的时候, 可以选择专门的人员. 比如看病时, 我们希望选择专门的医生. 修水管时, 选择专门的水管工.(或者叫技师,嘿嘿.)
这样的使用, 也不难. 我们可以重写task的分发和抽取方法即可.
class Task{
// 需要指定的workerid 指定你需要执行任务的技师
String workerId;
}
// 工作类
class Worker{
public void work(){
while(currentTime>startTime & currentTime<endTime){
if(queueTaskList.isNotEmpty()){
// 写的简单点. 我们可以判断当前头部任务是否是需要我来做的.
var task = queueTaskList.head();
if(task.workerId.equals(this.id)){
// 指定的技师是当前工人. 那么就由我来拯救你吧.
// 从任务池中接下任务, 然后执行任务.
var task = queueTaskList.pop();
task.perform();
}else{
// 休息10秒中. 等其他技师接下任务.
}
}else{
// 否则过1分钟过后. 再来查看工作池中是否有工作需要干.
}
}
}
}
多人不同工作 - 无相互关联
上面的讲述. 主要讲解. 我们如何让多人协作完成相同的一件事情. 当然, 在生活中, 我们经常遇到的情况却不是这样. 我们经常遇到的事情是, 需要同时执行2件不相同的事情. 还是以包饺子为例, 我们都需要买菜. 1. 需要买10斤肉. 2. 需要买10斤韭菜. 那么这2件事情, 同时也是可以让2个人来做的. 当然, 你可以可以让2个人来买这10斤肉. 全凭你的喜好. 我们的伪代码应当这样写:
// 买韭菜
Task buyChives
// 买肉
Task buyMeat.
// 准备2个工人分别干这个活
Thread worker1 = new Thread(buyMeat);
Thread worker2 = new Thread(buyChives);
// 发号司令 2个人开始干活
worker1.start();
worker2.start();
当然, 你可以可以让2个人来买这10斤肉. 全凭你的喜好. 此时, 伪代码应该可以这样写.
// 买韭菜
Task buyChives
// 买肉
Task buyMeat.
// 准备3个工人分别干这个活
Thread worker1_1 = new Thread(buyMeat);
Thread worker1_2 = new Thread(buyMeat);
Thread worker2 = new Thread(buyChives);
// 发号司令 3个人开始干活
worker1_1.start();
worker1_2.start();
worker2.start();
多人不同的工作 - 有相互关联
在生活中. 我们经常遇到的事情, 有时并不是单纯的买菜. 而是比较复杂的, 有相互依赖关系的2件, 或者多件事情. 那么我们又应该如何做呢?
还以包饺子为例. 我们需要人1. 买菜, 2. 包饺子 3. 下饺子. 这我们选择3波人同时开始做. 但是… 这3件事情是有依赖关系的. 为了追求方便性和速度. 我们约定, 2个人买菜. 2个人包饺子. 2个人下饺子. 并且包饺子人需要等买菜结束后才能执行. 下饺子必须等包饺子结束后才能执行. 那么, 这个流水线操作, 就形成了. 我们可以设置如下的开关.
// 买菜任务
Task bugVegetable
// 包饺子任务
Task makeDumplings
// 下饺子任务
Task cookDumplings
// 准备6个工人分别干这个活
Thread worker1_1 = new Thread(bugVegetable);
Thread worker1_2 = new Thread(bugVegetable);
Thread worker2_1 = new Thread(makeDumplings);
Thread worker2_2 = new Thread(makeDumplings);
Thread worker3_1 = new Thread(cookDumplings);
Thread worker3_2 = new Thread(cookDumplings);
// 买菜先行
worker1_1.start();
worker1_2.start();
// 等待买菜结束
worker1_1.join();
worker1_2.join();
// 包饺子第二
worker2_1.start();
worker2_2.start();
// 等待包饺子结束
worker2_1.join();
worker2_2.join();
// 下饺子第三
worker3_1.start();
worker3_2.start();
此处的等待当然在Java内, 可以使用Thread的Join方法或者CountDownLatch等工具类实现. 此处略.
锁
Java中的锁实现其实有非常多的种类. 但是日常生活中, 我们锁的抽象都是一样的. 所以此处就不再举现实的例子进行映射了.
- Java内的锁: Synchronized
- Java锁: ReentanckLock / ReentrackReadWriteLock
- 锁工具类: CountDownLatch / CycleBarrier / Semaphore
- 限流器: Semaphore
- 交流器: Exchanger
- 可视化同步关键字: volatile
锁的实现, 在Java内也是可以通过直接编译参数, 或者CAS进行实现的.
由于此处篇幅有限. 此处就不一一赘述了.
通信
Java内的通信其实可以分为线程间的信号通信和直接通信. 信号的通信主要是Thread的wait和notify方法. 当然还有ReentackLock中的condition方法. 直接通信的话, 可以通过方法写入和消息池2种.
- 信号通信: wait/notify/notifyAll | condition
- 消息通信: sendMessage(People people, Message message) | 消息池(消息通道)
异步操作
我们日常生活中. 做事经常不是同步的操作. 什么是同步操作, 什么是异步操作? 非常简单. 举个例子:
-
老板让小王买菜. 小王买菜回来之前, 老板都一直等待. 此处是同步操作.(怕不是老板是老板娘, 买菜的是老板. HAHA) -
老板让小王买菜. 小王买菜回来之前, 老板会做一些其他事情, 比如打游戏等待. 此处是异步操作.
### 同步伪代码
// 买菜任务
Task bugVegetable
Thread workerWang = new Thread(bugVegetable);
workerWang.start();
// 老板同步等待
workerWang.join();
### 异步伪代码
// 买菜任务
Task bugVegetable
Thread workerWang = new Thread(bugVegetable);
workerWang.start();
// 老板干其他事情 - 比如打游戏
game();
// 老板等待 - 老板等不及了. 要记账. 咋还没回来呢. 开始同步等待. 获取买菜结果.
workerWang.join();
### 异步伪代码 Future
// 买菜任务
Task bugVegetable
Future<Callable> workerWang = new Thread(bugVegetable);
workerWang.start();
// 老板干其他事情 - 比如打游戏
game();
// 老板等待 - 老板等不及了. 要记账. 咋还没回来呢. 开始同步等待. 获取买菜结果.
workerWang.get();
此处后2处的伪代码其实并没有什么区别的地方. 主要一个是使用了Java的Thread的join方法, 一个使用了Future的get方法. 其实本质并无区别.
原子操作
日常生活中的最小节点. 比如你做作业. 就不是最小节点. 做数学作业, 也不是. 计算一道数学题也不是. 但是"1+1=2"的计算工作可以认为是一个生活中的原子操作的.
安全的同步容器
比如我们有时候. 同时有2个人进行买菜, 他们都从账房进行取钱. 此时账房金额为1000, A取100, 将要把900写入. 此时B也取了100, 也要把900写入.此时B写入900. 此时A也写入900. 那么此时账房金额为900. 那么2个人都取了100. 那么谁亏了呢… HEI~ HEI~ 100快哪去了.
答案: 其实账房明显亏了. 被取了200. 写成取100. 账房实际还有800. 但是写900. 有100差值.
整体的流程是这样的:
- 账房1000.
- A取100. 计算出还剩900.
- B取100. 计算出还剩900.
- B写入900.
- A写入900
- 账房还剩800. 但是记账为900. 100错误.
那么怎么处理?
- 每次取的时候. 都基于本次-100. 但是此方法估计会减少为复数吧.(uptdate total=total-100 where total >=100;)
- 强制锁. 可以使用volatile, 也可使用sychronized的思路.
### synchronized的思路 (整个更新全部加锁. 悲观锁.)
> * 账房1000.
> * 锁定账房. A取100. 计算出还剩900.
> * B取100. 失败. 因为已经被A锁定了. 无法获取.
> * A写入900. 释放锁.
> * B获取锁. 锁定账房. B取100, 计算出还剩800.
> * B写入800. 释放锁.
> * 账房结束还剩800. 记账800. 此时就是正确的.
### volatile思路 (只在写入时加乐观锁)
> * 账房1000.
> * 锁定账房. A取100. 计算出还剩900.
> * B取100. 计算出还剩900.
> * A写入900. 释放锁.
> * B获取锁. 发现账房已经被A动过. 重新计算. 计算出等于800.
> * B写入800. 释放锁.
> * 账房结束还剩800. 记账800. 此时就是正确的.
工作池 & 工作池框架
生活中. 经常有一套方案. 比如指派2个人进行买菜, 2人进行包饺子. 那么 2人进行销售, 2人进行记账. 这样的方案, 我们经常认为时线程池. 在Java内我们非常多的线程池种类. 比如固定人数的线程池, 可变人数的线程池, 定时线程池. 当然为了让线程池这样的方案更容易使用, 我们还又包装了一层的Executors. 这不是本篇的主要议题. 那么就简单的简要的略过了.
总结
本文主体介绍了Java中的多线程与现实的使用场景. 明显人可以看出, 其中的Java相关的影子.
- 工人: Thread
- 线程同步: Join
- 线程异步: Future Callable
- 其他同步工具: CountDownLatch / CycleBarrier / Semaphere
- 锁: Synchronized / ReentackLock / 非锁工具 volatile
- 原子操作: AtomicInteger
- 消息通信: wait & notify / condition /消息Queue
- 线程池: ThreadPool / Executor
|