多线程的代码案例

目录

单例模式

饿汉模式

懒汉模式

阻塞队列

 生产者消费者模型意义:

阻塞队列使用方法

实现阻塞队列

阻塞队列实现生产者消费者模型

定时器

实现简单的定时器

工厂模式

线程池

为啥呢? 从池子里面取 比 创建线程 效率更高

线程池的创建

怎么填坑

ThreadPoolExecutor

线程数目设置

 实现线程池

 小结


两个设计模式: 单例模式, 工厂模式

单例模式

有些场景中希望有些类仅仅创建一个对象, 代码中很多管理数据的对象都是单例的, MySQL JDBC等.

人可能会出错, 需要编译器帮我们做出监督. 就比如 @Override 必须是方法重写.,在语法层面上没有对单例做出支持, 只能通过编程技巧实现

饿汉模式

刚开始就创建了实例举个例子:

//期望这个类能有唯一实例
class Singleton {//设置为静态变量在 Singleton 类被加载时会创建实例private static Singleton instance = new Singleton();//获取实例public static Singleton getInstance() {return instance;}//把构造方法设为 私有 , 类外面的代码无法 new 出类对象了.private Singleton() {};
}

注意:

1> 在类的内部提供线程的实例

2> 把构造方法设为 private ,避免其他代码创建实例.

懒汉模式

先判断是否需要创建实例举个例子:

//期望这个类能有唯一实例
class SingletonLazy {private static volatile SingletonLazy instance = null;//获取实例public static SingletonLazy getInstance() {if(instance == null) {synchronized (SingletonLazy.class) {if(instance == null) {instance = new SingletonLazy();}}}return instance;}//把构造方法设为 私有 , 类外面的代码无法 new 出类对象了.private SingletonLazy() {};
}

注意:

1> 第一次判断是否为空原因:

因为加锁开销很大, 而且可能涉及到锁冲突, 所以我们增加一次判断, 不为空直接返回 instance

2> 加锁的原因:

在本操作中会出现读取和修改的操作, 会出现两个都判断为空后创建多个实例的情况.

3> 使用 volatile 原因:

指令重排序问题

编译器为了提高效率, 可能调整代码的执行顺序, 但是必须保持代码逻辑不变, 单线程没问题, 但是多线程可能有问题.

new 操作, 可能触发指令重排序

new 操作分为三步:

1. 申请内存空间

2. 在内存空间上构造对象

3. 把内存地址给 instance

可能按照 123, 132顺序执行, 1一定先执行

在多线程下, 假设  t1线程   按照1 3 2 的顺序  执行1  3后, instance非空指向一个没初始化的非法对象, 这时      t2线程   在判断instance 不为空后, 直接返回一个非法对象, 导致出现bug

使用 volatile 保证不会出现指令重排序问题

阻塞队列

多线程代码中比较常用到的一种数据结构

特殊的队列

1> 线程安全

2> 带有阻塞特性

a) 如果队列为空, 继续出队列, 就会发生阻塞, 阻塞到其他线程往队列里添加元素位置为止

b) 如果队列为满, 继续入队列, 也会发生阻塞, 阻塞到其他线程从队列中取走元素位置为止.

意义: 实现 " 生产者消费者模型 " 一种常见的多线程代码编写方式

举个例子: 包饺子

1> 每个人分别负责擀饺子皮和包饺子

2> 当擀饺子皮快了 就会在 放饺子皮的盖帘满的时候停下来等包饺子的

3> 当包饺子快了 就会停下来等 擀饺子皮的

盖帘就相当于阻塞队列

生产者 把生产出来的内容放到阻塞队列中

消费者 从阻塞队列中获取元素

 生产者消费者模型意义:

1> 解耦合

两个模块联系越紧密, 耦合就越高, 这个模型让耦合降低

2> 削峰填谷

服务器 A 给服务器 B发起请求, 不同服务器消耗的硬件资源不一样, A收到的请求发给B可能就挂了.使用削峰填谷让 B 接受的请求按照 B 的原有节奏处理情况.(这种情况一般不会持续存在, 就好比学校抢课的情况), 峰值过后 B把积压的数据处理掉

阻塞队列使用方法

在 Java 标准库里, 已经提供了现成的 阻塞队列直接使用

在标准库里, 针对 BlockingQueue 提供了两种最重要的实现方式

1> 基于数组

2> 基于链表

BlockingQueue 一般不适用 Queue 中的一些方法, 因为他们不具备阻塞的特性. 

一般使用 (put 阻塞式的入队列), (take 阻塞式的出队列)

示例: 

public class Test {public static void main(String[] args) throws InterruptedException {BlockingDeque<String> queue = new LinkedBlockingDeque<>();queue.put("111");queue.put("222");queue.put("333");queue.put("444");String elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);}
}

最后一次输出时发生了阻塞.

实现阻塞队列

基于普通队列加上阻塞和线程安全

普通队列基于数组 或者 基于链表

基于数组实现队列理解成一个环

class MyBlockingQueue {private String[] data = new String[1000];// 队列的起始位置private int head = 0;// 队列的结束位置的下一个位置private int tail = 0;//队列中有效元素的个数private int size = 0;//提供的方法 入队列 出队列public void put(String elem) throws InterruptedException {synchronized (this) {while(size == data.length) {this.wait();}data[size] = elem;tail++;if(tail == data.length) {tail = 0;}size++;//这个 notify 用来唤醒 take 中的 waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while(size == 0) {this.wait();}String ret = data[head];head++;if(head == data.length) {head = 0;}size--;//这个 notify 用来唤醒 put 中的 waitthis.notify();return ret;}}
}

wait 除了可以用 notify 唤醒, 还可以用 interrupt 唤醒, 直接整个方法结束了, 因为使用了 throws 抛出异常, 这是没有什么事

如果使用 try catch 方式就会出现bug, 让 tail 把指向的元素覆盖掉了, 然后弄丢了一个元素, 而且 size 也会比数组最长长度还大.(此处不理解看http://t.csdnimg.cn/OBwXN -->中断一个线程目录)

所以在wait 返回的时候进一步确认是否当前队列是满的不是, 如果是满的继续进行wait

所以直接使用 while 判定是否是满的.

为了避免内存可见性问题, 把 volatile 加好

阻塞队列实现生产者消费者模型

package Demo2;import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;class MyBlockingQueue {private String[] data = new String[1000];// 队列的起始位置private volatile int head = 0;// 队列的结束位置的下一个位置private volatile int tail = 0;//队列中有效元素的个数private volatile int size = 0;//提供的方法 入队列 出队列public void put(String elem) throws InterruptedException {synchronized (this) {while(size == data.length) {this.wait();}data[tail] = elem;tail++;if(tail == data.length) {tail = 0;}size++;//这个 notify 用来唤醒 take 中的 waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while(size == 0) {this.wait();}String ret = data[head];head++;if(head == data.length) {head = 0;}size--;//这个 notify 用来唤醒 put 中的 waitthis.notify();return ret;}}
}public class Test {public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue();// 消费者Thread t1 = new Thread(() -> {while(true) {try {String result = queue.take();System.out.println("消费元素: " + result);Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 生产者Thread t2 = new Thread(() -> {int num = 1;while(true) {try {queue.put(num+ " ");System.out.println("生产元素: " + num);num++;} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

定时器

约定一个时间, 时间到达之后执行某个代码逻辑, 在网络通信中很常见

 在 标准库 中有现成定时器的实现

    public static void main(String[] args) {Timer timer = new Timer();// 给定时器安排了一个任务, 预定在 xxx 时间去执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器任务");}}, 2000);System.out.println("程序启动!");}

使用匿名内部类的写法继承 TimerTask 创建出实例, 目的时重写 run, 描述任务的详细情况

当前代码也是多线程, timer 里面包含一个线程, 下图是运行结果

可以发现整个进程没有结束, 因为 Timer 内部的线程阻止了进程结束.

 Timer 里面可以安排多个任务.

    public static void main(String[] args) {Timer timer = new Timer();// 给定时器安排了一个任务, 预定在 xxx 时间去执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}}, 3000);System.out.println("程序启动!");timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000");}}, 2000);System.out.println("程序启动!");timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000");}}, 2000);System.out.println("程序启动!");}

实现简单的定时器

1> Timer 中需要有一个线程, 扫描任务是否到时间了, 可以执行了

2> 需要一个数据结构把所有任务保存起来(使用优先级队列) 

3> 创建一个类, 通过类的对象描述一个任务(至少要包含任务内容和时间)

 其中需要记录, 绝对的时间.

import java.awt.*;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;// 通过这个类, 描述一个任务
class MyTimerTask implements Comparable<MyTimerTask> {// 执行的任务private Runnable runnable;// 执行任务的时间private long time;// 此处的 delay 就是 schedule 方法传入的 "相对时间"public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTimerTask o) {// 让队首元素是最小时间的值return (int) (this.time - o.time);// 让队首元素是最大时间的值//return (int) (o.time - this.time);}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}// 自己的定时器
// 添加元素和扫描线程是不同线程操作同一个队列, 需要加锁 <--原因之一
class MyTimer {// 使用一个数据结构, 保存所有的任务private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 使用这个对象作为锁对象private Object locker = new Object();public void schedule(Runnable runnable, long delay) {synchronized(locker) {queue.offer(new MyTimerTask(runnable, delay));locker.notify();}}// 扫描线程public MyTimer() {// 创建一个扫描线程Thread t = new Thread(() -> {// 扫描线程需要不停扫描看是否到达时间while (true) {try {synchronized (locker) {// 不要使用 if 作为 wait 的判定条件, 应使用while// 使用 while 是为了在唤醒之后 在再次确认一下条件while (queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();// 比较一下当前的队首元素是否可以执行了long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {// 执行任务task.getRunnable().run();//执行完了, 就从队列中删除queue.poll();} else {// 不可执行, 先等着, 等待下一轮的循环判定locker.wait(task.getTime() - curTime);}}}catch (InterruptedException e) {e.printStackTrace();}}});t.start();}
}public class Demo2 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}}, 1000);}
}

工厂模式

线程池

线程创建/销毁 比 进程快, 但是进一步提高创建/销毁的频率, 线程的开销也不能忽视了

两种提高效率的方法:

1> 协程 (轻量级线程) 

相对于线程, 把系统调度的过程给忽略了,(程序猿手动调度), 当下比较流行(Java 标准库没有协程)

2> 线程池

兜底, 使线程不至于很慢

例子: 我是个妹子, 在谈男朋友, 一段时间后, 我不想和他好了, 就冷暴力然后分手, 分手之后再去找另一个小哥哥, 然后和另一个小哥哥好上了. 

线程池就是我在谈第一个男朋友的时候就同时和其他小哥哥搞暧昧(培养感情), 哪天想分手了直接分, 然后无缝衔接

线程池: 在使用第一个线程的时候, 提前把 2, 3, 4, 5线程创建好(培养感情), 后续想使用新的线程不必创建, 直接使用(创建线程的开销降低了)

为啥呢? 从池子里面取 比 创建线程 效率更高

从池子里取, 就是纯粹用户态操作

创建新的线程需要 用户态 + 内核态 相互配合 完成

操作系统是由 内核 + 配套的应用程序 构成

内核 是系统最核心的部分, 创建线程操作需要调用系统 api, 进入到内核中, 按照内核态的方式来完成一系列动作

当你想要创建线程的时候, 内核需要给所有进程提供服务, 不可控, 难以避免会做一些其他的事导致效率减低

线程池的创建

Java标准库提供了写好的线程池.

创建线程池对象并没有 new , 而是通过专门的方法返回了一个线程池对象(工厂模式), 通常创建对象使用 new , new 就会触发类的构造方法, 但构造方法存在一定的局限性. 工厂模式是给构造方法填坑的.

怎么填坑

我们构造一个对象希望有多种构造方式, 这就需要多个构造方法, 但是构造方法的名字必须是类名, 不同的构造方法只能通过 重载区分, 但是如果实现方法不一样, 但是参数类型/个数一样咋办呢?

使用工厂设计模式, 使用普通的方法代替构造方法完成初始化工作, 普通方法使用名字区分.

 Executors 是一个 工厂类, newCachedThreadPool 是工厂方法, 使用静态方法通过类名调用

工厂方法有很多, 上述方法创建出来的线程池对象的线程数目可以动态适应, 随着王线程池里面添加任务, 线程池中的线程自动创建, 创建出来在池子里保留一定时间以备后续使用.

这个方法是固定的线程池, 调用方法时手动指定创建几个线程

 还用很多其他线程池上面介绍的两种用的更多一点

ThreadPoolExecutor

上述工厂方法生成的线程池本质上是对 类(ThreadPoolExecutor) 的封装

核心方法:

1> 添加任务

2> 构造

举例:  1> 添加任务 (简单) 

使用 submit 把任务交给线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo3 {public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(4);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}

 2> 构造方法 (重点)

构造方法中参数很多[经典面试题]

在 juc 包里面, 并发编程相关内容 

全部参数 如下图:

对这 4 种情况 举个例子:

我有 任务 A 要做, 朋友来让我帮忙做任务 B, 这时我有 4 种回应方法.

1> 我心态崩了, 大哭. 抛出异常

2> 我对朋友说你自己做, 朋友自己做任务 B

3> 我的任务 A 不做了, 就去帮朋友

4> 我直接拒绝帮忙, 我仍然做任务 A , 朋友也不做任务 B 了

线程数目设置

使用线程池需要设置线程的数目, 设置多少合适?

具体数目是不对的, 需要实际情况分析

原因:

一个线程执行代码主要有两类:

1> cpu 密集型: 代码主要是进行 算术运算/逻辑判断

2> IO密集型: 代码里主要进行的是 IO 操作

如果是 1>  这个时候线程池的数量不要超过 N (设 N 就是极限), 比 N 更大, 就无法提高效率了, cpu吃满了, 线程越多反而增加调度的开销

如果是 2>  不吃 CPU, 此时设置的线程数可以超过 N, 一个核心可以通过调度的方式来并发执行.

 实现线程池

class MyThreaPool {// 任务队列private BlockingDeque<Runnable> queue = new ArrayBlockingQueue<>();// 通过这个方法, 把任务添加到队列中public void submit(Runnable runnable) throws InterruptedException {//此处策略是第 5 种, 拒绝策略, 阻塞等待queue.offer(runnable);}public MyThreaPool(int n) {// 创建出 n 个线程, 负责执行上述队列中的任务for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {// 让这个线程从队列中消费任务,并进行执行try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}

 小结

认真学习各种多线程代码实例, 理解其中的含义, 将各个代码的的易错点分析透彻

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/12845.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图解堆排序【一眼看穿逻辑思路】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 目录 1、堆的概念2、实现堆排序前的准备工作3、堆排序的思路3.1 第一步3.2 第二步 4、结语 1、…

音视频捕捉技术:LCC382 SDI采集卡深度解析

在日新月异的多媒体时代&#xff0c;高质量的音视频采集已成为众多领域不可或缺的一环。为此&#xff0c;灵卡科技精心打造了LCC382 —— 一款集高效性、灵活性与前沿技术于一身的SDI输入与环出、HDMI输出音视频采集卡&#xff0c;旨在满足从专业直播、视频会议到医疗影像、安防…

网页版Figma汉化

最近学习Figma&#xff0c;简单介绍一下网页版Figma的汉化方法 1.打开网址&#xff1a;Figma软件汉化-Figma中文版下载-Figma中文社区 2.下载汉化插件离线包 解压汉化包 3.点开谷歌的管理扩展程序 4.点击加载已解压的扩展程序&#xff0c;选择刚刚解压的包 这样就安装好了汉化…

QT状态机2-含终止状态的嵌套状态机

#include "MainWindow.h" #include "ui_MainWindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)

前馈神经网络FNN、多层感知机MLP和反向传播推导

目录 一、前馈神经网络FNN 激活函数的使用 二、多层感知机MLP MLP的典型结构 多层感知机MLP的特点 和前馈神经网络FNN的区别 三、传播推导 1、前向传播(Forward propagation) &#xff08;1&#xff09;输入层到隐藏层 &#xff08;2&#xff09;隐藏层到输出层 2、…

Java面试八股之WeakHashMap的工作原理

简述WeakHashMap的工作原理 弱键&#xff08;Weak Keys&#xff09;&#xff1a; WeakHashMap 的键&#xff08;keys&#xff09;是通过 WeakReference 弱引用进行封装的。弱引用是一种特殊的引用类型&#xff0c;它不会阻止所引用的对象被垃圾收集器回收。这意味着&#xff…

冥想训练具体方法有哪些|流静冥想

冥想是一种身体的放松和敏锐的警觉性相结合的状态。 每日练习的好处远不止你花在集中注意力的那几分钟。桑托雷利是建在乌斯特的马萨诸塞大学医学院的减压诊所的所长&#xff0c;她也是《自愈》的作者&#xff0c;她说&#xff1a;"冥想是一种工具&#xff0c;通过练习&a…

云手机的优缺点分析

云手机&#xff0c;作为云计算领域的创新&#xff0c;致力于提供更为灵活的移动设备体验&#xff0c;特别适用于那些希望在不同设备之间无缝切换的用户。虽然云手机带来了一系列优势&#xff0c;但也伴随着一些挑战&#xff0c;比如网络延迟可能会影响用户体验&#xff0c;特别…

网络安全|隐藏IP地址的5种不同方法

隐藏计算机的IP地址在互联网在线活动种可以保护个人隐私&#xff0c;这是在线活动的一种常见做法&#xff0c;包括隐私问题、安全性和访问限制内容等场景。那么如何做到呢?有很5种方法分享。每种方法都有自己的优点和缺点。 1. 虚拟网络 当您连接到虚拟服务器时&#xff0c;您…

Dubbo配置上的一些概念

对于dubbo在spring中我们可能看到有如下配置&#xff08;可参考Schema 配置参考手册 | Apache Dubbo&#xff09;&#xff1a; dubbo:application:id: dubbo-account-examplename: dubbo-account-example# 是否启用 Dubbo 的 QoS&#xff08;Quality of Service&#xff09;服…

Blender雕刻建模_笔刷

1.雕刻模式 雕刻Scuplt&#xff0c;一种常用的建模方式 -选中物体&#xff0c;进入雕刻模式 -重构网格&#xff08;修改体素大小&#xff0c;点击重构网格&#xff09;给物体添加更多面 -选择笔刷&#xff0c;雕刻 -退出雕刻模式 2.重构网格 一种按体积的细分方式&#xf…

openstack部署nova中出现的问题:

[rootcontroller nova]# su -s /bin/sh -c “nova-manage db sync” nova /usr/lib/python2.7/site-packages/pymysql/cursors.py:170: Warning: (1831, u’Duplicate index block_device_mapping_instance_uuid_virtual_name_device_name_idx. This is deprecated and will be…

Springboot+MybatisPlus如何实现带验证码的登录功能

实现带验证码的登录功能由两部分组成&#xff1a;&#xff1a;1、验证码的获取 2、登录&#xff08;进行用户名、密码和验证码的判断&#xff09; 获取验证码 获取验证码需要使用HuTool中的CaptchaUtil.createLineCaptcha()来定义验证码的长度、宽度、验证码位数以及干扰线…

这个notebook集合,赞

这几天在Github上看到一个数据科学仓库&#xff0c;汇总了很多Python notebook代码&#xff0c;主要是数据方向。 项目地址&#xff1a; https://github.com/donnemartin/data-science-ipython-notebooks 其中包括了pandas、numpy、matplotlib、scikit-learn、tensorflow、sp…

c++ visualstudio2017 opencv debug源码 windows配置

源码下载和cmake opencv源码和opencv-contribue文件夹的层级目录 在opencv-4.4.0中新建build文件夹&#xff0c;并启动cmake-gui 配置如下&#xff0c;使用vs2017 x64, 需要注意contrib文件夹的设置&#xff0c;如下方蓝色所示&#xff0c;依次点击Configure和Generate 在bu…

半小时搞懂STM32知识点——UART

1.UART 1.1为什么要使用UART这种协议?介绍一下UART及其特点 成本低&#xff0c;硬件简单&#xff0c;数据格式灵活&#xff1b; 低速全双工异步串行通信 1.2 UART数据帧格式&#xff1f; 起始位&#xff08;1&#xff09;&#xff0b;数据位&#xff08;5-8&#xff09; 校验位…

docker-java 操作docker

部署docker 10分钟学会Docker的安装和使用_docker安装-CSDN博客文章浏览阅读2.5w次&#xff0c;点赞44次&#xff0c;收藏279次。文章目录Docker简介Docker安装Windows安装Linux安装CentOS安装Ubuntu安装最近花了些时间学习docker技术相关&#xff0c;在此做一些总结&#xff0…

你还在手动加好友?试试这款神器,释放双手自动添加!

你还在手动添加微信好友吗&#xff1f;尤其是在忙碌的时候&#xff0c;手动加好友不仅费时又很容易出错。试试这个自动添加好友神器——个微管理系统&#xff0c;释放你的双手&#xff0c;轻松拓展好友列表&#xff01; 1、多号同时登录在线 系统支持多个微信号同时登录在线&…

数据结构——二叉树知识点详解!

引言&#xff1a;本篇博客将详细介绍到数据结构中的又一位大将——二叉树。它也是我们目前学到的第一个非线性的数据结构。并且本章将学到的概念居多&#xff0c;希望大家可以理解并牢记。 更多有关C语言和数据结构知识详解可前往个人主页&#xff1a;计信猫 目录 一&#xff0…

AIGC行业现在适合进入吗

AIGC行业目前正处于快速发展阶段,市场需求正处于爆发期,上大学网&#xff08;www.sdaxue.com&#xff09;认为&#xff0c;对于有兴趣的个人或企业而言&#xff0c;现在可能是一个适合进入的时机&#xff0c;以下是具体的分析&#xff0c;供大家参考&#xff01; 一、AIGC行业前…