并发编程(2)
线程之间的协作(等待和唤醒机制)–生产者消费者模型
-
对于消费者要消费产品时,可以使用轮询的方式对产品进行判断其是否存在,存在则进行消费,这种方式的弊端就是,实时性查,性能开销大;使用等待唤醒机制可以解决此弊端
-
等待唤醒是基于类的方法obj.wait()和obj.notify(),调用此方法之前都必须持有锁
public final void wait() throws InterruptedException public final void notify() public final void notifyAll()
-
notifyAll()可以唤醒使用此类调用wait()方法的线程
-
notify()在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm;而hotspot对notofy()的实现并不是我们以为的随机唤醒, 而是“先进先出”的顺序唤醒!
参考链接:https://www.jianshu.com/p/3bba64487922
等待唤醒机制的范式
-
生产者
- 获取容器的锁
- 当商品小于最小数量则生产商品放入容器
- 当商品数量大于设定值则唤醒消费者
- 当商品大于最大数量则停止生产
-
消费者
- 获取容器的锁
- 当商品数量小于设定值则停止消费进行等待
- 被唤醒时继续进行消费
import java.util.LinkedList;
/**
* 简单实现生产者消费者模型
*
* @author maolin yuan
* @version 1.0
* @date 2021/8/27 16:00
*/
public class ProducerConsumer {
private static final LinkedList<Integer> INTEGERS = new LinkedList<>();
private static final int MID = 25;
private static final int MIN = 3;
private static final int MAX = 50;
static class Producer implements Runnable {
int i = 0;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
synchronized (INTEGERS){
// 当商品数量小于设定的最小值时进行生产
if (INTEGERS.size() < MIN){
System.out.println("生产者进行生产。。");
while (INTEGERS.size() < MAX){
INTEGERS.add(i);
i++;
if (INTEGERS.size() == MID){
System.out.println("生产者唤醒消费者进行消费。。");
INTEGERS.notifyAll();
}
}
}
if (INTEGERS.size() >= MAX){
System.out.println("生产者等待。。");
try {
INTEGERS.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
synchronized (INTEGERS){
if (INTEGERS.size() == MID){
System.out.println("商品数量低于设定值唤醒生产者。。");
INTEGERS.notifyAll();
}
if (INTEGERS.size() < MIN){
System.out.println("商品卖完了消费者等待并唤醒生产者。。");
INTEGERS.notifyAll();
try {
INTEGERS.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer first = INTEGERS.removeFirst();
System.out.println(first);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread pro =new Thread(new Producer());
pro.setDaemon(true);
pro.start();
Thread con =new Thread(new Consumer());
con.setDaemon(true);
con.start();
Thread.sleep(50);
}
}
等待超时模式
- 定义一个超时时间,等待的线程超过这个时间会继续执行返回一个默认结果
// 设定超时时间为T
long overtime = System.currentTimeMillis() + T;
long remain = T;
while(result 不满足条件 && remain > 0){
// 等待固定时间
wait(T);
// 被唤醒之后还不满足条件,更新remain
remain = overtime - System.currentTimeMillis();
}
return result;
join()方法使别的线程插队
public final void join() throws InterruptedException
- 在线程中调用别的线程的join()方法,此线程会阻塞,等待别的线程执行完才会执行
/**
* 测试线程join方法
* @author maolin yuan
* @version 1.0
* @date 2021/8/27 17:40
*/
public class ThreadJoin {
static class ThreadDemo implements Runnable{
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("demo线程执行完成");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("main 开始执行");
Thread demo = new Thread(new ThreadDemo());
demo.start();
demo.join();
System.out.println("main 完成执行");
}
}
// main 开始执行
// demo线程执行完成
// main 完成执行
线程调用yield()、sleep()、wait()、notify()方法对锁的影响
- yield()、sleep()方法都不会释放锁
- 调用yield()方法释放了执行权之后cpu还是有可能会选择它,调用sleep()的线程cup不会选择它
- 只能在持有锁的情况下调用wait()方法,并且只能用锁调用wait()方法,调用wait()方法之后会立即释放锁,当wait()方法返回(被唤醒)之后会重新持有锁
- 只能在持有锁的情况下调用notify()方法,并且只能用锁调用notify()方法,调用notify()之后不会立即释放锁,只有当持有锁的代码块执行完之后才会释放锁
Java的并发工具类(Fork-Join)
- 分而治之的编程思想,讲一个问题分解成若干个相互独立小问题,再把所有小问题的解合并起来得到最终的解(通过递归和多线程实现),例如规模为N的问题,N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解
假如相加一个长度为N的数组中所有的元素,可以把数组分为K份,分别计算每一份的结果,最后把结果相加得到最终结果
- 工作密取(workStealing)指当多个线程同时执行一系列的任务时,任意一个线程执行完成之后,会帮助执行其他线程的任务,使线程被最大限度利用起来
- Fork-Join标准使用范式
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
/**
* 使用fork-join同步执行并返回
* @author maolin yuan
* @version 1.0
* @date 2021/8/30 10:42
*/
public class ForkJoinTest {
static class MyTask extends RecursiveTask<Long> {
// 需要计算的数组
int[] ints;
// 开始的索引
int start;
// 结束索引
int end;
// 设定的阈值(固定值)
int sdz;
public MyTask(int[] ints, int start, int end, int sdz) {
this.ints = ints;
this.start = start;
this.end = end;
this.sdz = sdz;
}
/**
* The main computation performed by this task.
*
* @return the result of the computation
*/
@Override
protected Long compute() {
if ((end - start) < sdz) {
// 长度小于设定值时直接计算结果
long count = 0;
for (int j = start; j < end; j++) {
try {
// 增加执行时间
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count = count + ints[j];
}
return count;
} else {
// 长度大于设定值时拆分任务(可以拆分两个或者多个)
// 计算中间位置的索引
int i = (end + start) / 2;
MyTask myTask1 = new MyTask(ints, start, i, sdz);
MyTask myTask2 = new MyTask(ints, i, end, sdz);
// 执行所有拆分的子任务
invokeAll(myTask1, myTask2);
// 返回的结果是所有任务相加
return myTask1.join() + myTask2.join();
}
}
}
public static void main(String[] args) {
int aa = 5000;
int[] ints = new int[aa];
for (int i = 0; i < aa; i++) {
ints[i] = (int) (Math.random() * 100);
}
// 使用Fork-Join
long time = System.currentTimeMillis();
// 创建线程池
ForkJoinPool pool = new ForkJoinPool();
// 创建任务
MyTask myTask = new MyTask(ints, 0, ints.length, aa / 10);
// 使用线程池执行任务
pool.invoke(myTask);
// 获取任务结果
Long join = myTask.join();
System.out.println("结果是:" + join);
System.out.println("时间是:" + (System.currentTimeMillis() - time));
// 不使用Fork-Join
time = System.currentTimeMillis();
long l = 0;
for (int anInt : ints) {
try {
// 增加执行时间
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
l = l + anInt;
}
System.out.println("结果是:" + l);
System.out.println("时间是:" + (System.currentTimeMillis() - time));
}
}
// 结果是:248063
// 时间是:1239
// 结果是:248063
// 时间是:8143
当注释掉增加执行时间的代码时,为什么使用Fork-Join时间还反而变长了,是因为线程上下文切换耗费了大部分时间,而计算结果的时间只占很少一部分。
- 使用Fork-Join查找异步文件
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
/**
* 使用fork-join异步查找yml结尾的文件
* 子任务不确定数量
* @author maolin yuan
* @version 1.0
* @date 2021/8/30 15:05
*/
public class FindFiles {
static class Find extends RecursiveAction {
private final File file;
public Find(File file) {
this.file = file;
}
/**
* The main computation performed by this task.
*/
@Override
protected void compute() {
List<Find> ff = new ArrayList<>();
if (file.exists()) {
File[] files = file.listFiles();
assert files != null;
if (files.length != 0) {
for (File f : files) {
if (f.isDirectory()) {
// 如果file是文件夹则创建子任务
ff.add(new Find(f));
} else if (f.getAbsolutePath().endsWith("yml")) {
System.out.println("YML文件:" + f.getAbsolutePath());
}
}
if (!ff.isEmpty()){
for (Find find : invokeAll(ff)) {
find.join();
}
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
Find task = new Find(new File("I:\\MyFile"));
// 异步执行使用execute
pool.execute(task);
task.join();
// Thread.sleep(2000);
}
}
常用的其他并发工具类
- CountDownLatch(闭锁):创建一个计数器,指定数值,调用await()方法可以阻塞当前线程,其他线程调用countDown()方法使数值减一,当数值为0时,被阻塞的方法将继续执行,因此可以控制线程执行的先后顺序
- CyclicBarrier(栅栏):创建一个栅栏,指定数值,任意线程会在调用await()的地方被阻塞,只有当所有的线程都到达await()的地方时,阻塞才放开,让所有被阻塞的线程继续执行;也可以在构造方法中传入一个runnable,使阻塞放开之后执行此任务
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
/**
* 使用CountDownLatch
* @author maolin yuan
* @version 1.0
* @date 2021/8/30 15:41
*/
public class UseConcurrentUtil {
private static final CountDownLatch LATCH = new CountDownLatch(3);
private static final CyclicBarrier BARRIER = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("所有线程等待结束");
}
});
static class ThreadTest extends Thread {
@Override
public void run() {
System.out.println(getName() + "线程开始执行");
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
LATCH.countDown();
System.out.println(getName() + "线程执行完成");
}
}
static class BarrierTest extends Thread {
@Override
public void run() {
System.out.println(getName() + "线程开始执行");
try {
Thread.sleep((int) (Math.random() * 1000));
BARRIER.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(getName() + "线程执行完成");
}
}
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
System.out.println("主线程开始等待");
for (int i = 0; i < 3; i++) {
new ThreadTest().start();
}
LATCH.await();
System.out.println("主线程继续执行");
System.out.println("===============");
System.out.println("BARRIER测试开始");
for (int i = 0; i < 3; i++) {
new BarrierTest().start();
}
BARRIER.await();
Thread.sleep(1000);
System.out.println("BARRIER测试完成");
}
}
// 主线程开始等待
// Thread-0线程开始执行
// Thread-1线程开始执行
// Thread-2线程开始执行
// Thread-0线程执行完成
// Thread-1线程执行完成
// Thread-2线程执行完成
// 主线程继续执行
// ===============
// BARRIER测试开始
// Thread-4线程开始执行
// Thread-3线程开始执行
// Thread-5线程开始执行
// 所有线程等待结束
// Thread-4线程执行完成
// Thread-3线程执行完成
// Thread-5线程执行完成
// BARRIER测试完成