多线程基础篇(多线程案例)

文章目录

  • 多线程案例
      • 1、单例模式
        • 1)饿汉模式
        • 2)懒汉模式
        • 3)线程安全吗??
        • 4)解决懒汉模式线程安全问题
        • 5)解决懒汉模式内存可见性问题
      • 2、阻塞队列
        • 1) 阻塞队列是什么?
        • 2) 生产者消费者模型
          • 1.生产者消费者模型的优势
          • 2.标准库中的阻塞队列
        • 3) 拟实现阻塞队列
      • 3、定时器
        • 1) 标准库中的定时器
        • 2) 模拟实现定时器
      • 4、线程池
        • 1) 工厂模式
        • 2) 标准库中的线程池
          • 1.ThreadPoolExecutor类
        • 3) 模拟实现线程池

多线程案例

1、单例模式

单例模式是校招中最常考的设计模式之一。

啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有
一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照
这个套路来实现代码, 也不会吃亏.

应用场景:有时候,希望对象在程序中只有一个实例(对象),也就是说只能new一次。由此就出现了单例模式。

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.

单例模式具体的实现方法,分成“懒汉”和“饿汉”两种。

1)饿汉模式

含义:当前代码中是比较迫切的,在类加载的时候就立刻,把实例给创建出来。

class Singleton {private static Singleton instance = new Singleton();private Singleton(){}public static Singleton getInstance() {return instance;}
}
public class Demo3 {public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();//Singleton s3 = new Singleton();       无法创建出来,由于上面的构造方法是private的System.out.println(s1 == s2);//System.out.println(s1 == s3);}
}

上述代码的执行时机,是 Singleton 类被 jvm 加载的时候。Singleton类会在 JVM 第一次使用的时候被加载。也不一定是在程序一启动的时候.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用static修饰的雷属性,由于每个类的类对象是单例的。类对象的属性(static),也就是单例的了。

单例模式中对我们的代码做出一个限制:禁止别人去new这个实例,也就是把构造方法改成private。

此时,我们要是去new这个实例,就会报错。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这时,我们能够使用的实例有且只有一个,就是类字被加载出来的同时,会进行实例化。


2)懒汉模式

含义:在真正用到的时候来进行创建实例,不用到则不会产生其他开销。(更推荐使用)

class SingletonLazy {private static SingletonLazy instance = null;private SingletonLazy() {}public static SingletonLazy getInstance() {if(instance == null) {instance = new SingletonLazy();}return instance;}
}
public class Demo4 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}

懒汉模式与饿汉模式极其相似,只是在实例化的时机上有所不同。


3)线程安全吗??

接下来我们要去考虑 懒汉模式 和 恶汉模式 在多线程模式下会不会出问题呢????在多线程调用 getInstacne 的情况下,哪个模式是线程安全的(没有Bug)??

对于饿汉模式:多个线程同时读取同一个变量,就不会出现线程安全问题。

对于懒汉模式:则会出现线程安全问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

🎋 简单理解:当我们按照顺序来执行,就会发现,此时创建出两个对象,程序不再是单例模式了!!

🎋 深入解读:第二次 new 操作,就把原来 instacne 的引用给修改了,之前 new 出来的对象就立刻被 gc 回收了,最后只剩下一个对象。

在通常印象中,new 一个对象并不是一个很复杂的过程,但实际上 new 一个对象开销是非常大的!!!!如果服务器一启动就需要加载 100G 的数据存到内存中,都是通过一个对象来管理,那我们 new 这个对象时,就需要消耗 100G 的内存,花费很长时间。


4)解决懒汉模式线程安全问题

提到如何解决线程安全问题,想到的就是加锁以及内存可见性问题。

先考虑一种加锁方式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这种写法是错误的,没有解决上述线程安全问题。因为这个操作本身就是原子的,再加锁也起不到实质上的作用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正确加锁方式是把锁加到外面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

如图所示,此时再执行线程2的时候,就能发现 instance 是一个非 null 的值。

上述代码还会出现一个问题,程序中频繁的出现加锁操作。

加锁是一个成本很高的操作,加锁可能会引起阻塞等待。因此得到加锁的基本原则就是:非必要不加锁。不能无脑加锁,频繁加锁会影响程序执行效率。


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

思考:后续每次调用getInstance都要加锁了,是必要的嘛???

不是的。每次调用,都会加锁。实际上只有第一次调用有必要加锁,后面都不需要。这里把不该加锁的地方给加锁了,就会很影响程序的效率。

懒汉模式线程不安全,主要是在首次 new 对象的时候,才存在问题。一旦把对象new好了之后,后续再调用getInstance,都没事了!!

上述代码在首次调用和后续调用都加锁了,因此降低了程序的执行效率。

对代码进行调整:先判定是否要加锁,再决定是不是真的加锁

public static SingletonLazy getInstance() {if (instacne == null) {synchronized (SingletonLazy.class){if(instance == null) {instance = new SingletonLazy();}}}return instance;
}

可以看到,在外层又加了一个判断条件。第一个条件是为了判断是否要加锁,第二个条件是判断是否要创建对象

同一个条件,写了两遍,这个合理嘛???

合理的不得了。

如果在单线程中,两个同样的判定,结果一定是一样的。但对于多线程来说,可能会涉及到阻塞。阻塞多久并不知道,第二个条件和第一个条件之间可能会间隔非常长的时间(沧海桑田),在这个很长的时间间隔里,可能别的线程就把 instance 给改了。


5)解决懒汉模式内存可见性问题
public static SingletonLazy getInstance() {if (instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;
}

在多线程模式中,第一个线程,在修改完 instance 之后,就结束了代码块,释放了锁。第二个线程就能够获取到锁了,从阻塞中恢复了。

问题出现了,第二个线程这里进行的“读操作”一定能读取到第一个线程修改之后的值吗??

答案是不一定,可能会出现内存可见性问题。这里只是分析了一下,可能存在这样的风险。编译器实际上在这个场景中是否会触发优化,打上问号。因此,给 instance 加上一个 volatile 是更为稳妥的做法。

加上 volatile 还有另外一个用途,就是避免此处赋值操作的指令重排序!

指令重排序:也是编译器优化的一种手段,是保证原有执行逻辑不变的前提下,对代码执行顺序进行调整。使调整之后的执行效率提高。

单线程中,这样的重排序一般没事。但是多线程下,就会出现问题。

instance = new SingletonLazy()

像上面这句代码,一句代码在编译器中由三句指令构成。

  1. 给对象创建出内存空间,得到内存地址。
  2. 在空间上调用构造方法,对对象进行初始化。
  3. 把内存地址,赋值给 instance 引用。

在这里插入图片描述

给 instacne 加上 volatile 之后,此时针对 instance 进行的赋值操作,就不会产生上述的指令重排序了。必须按照 1 2 3 的顺序执行,不会出现 1 3 2 的执行顺序。

2、阻塞队列

1) 阻塞队列是什么?

阻塞队列是一种特殊的队列,带有阻塞功能。也遵守 “先进先出” 的原则。

阻塞队列能是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素。
  • 当队列空的时候,,继续出队列也会阻塞,直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景就是 “生产者消费者模型”。这是一种非常典型的开发模型


2) 生产者消费者模型

在该模型中,生产者负责生产数据,并将其放入一个缓冲区中。消费者负责从缓冲区中取出数据并进行消费。

本质上来说就是通过一个容器来解决生产者和消费者的强耦合问题

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

1.生产者消费者模型的优势

减少任务切换开销,减少锁竞争。

  1. 阻塞队列会使生产者和消费者之间 解耦合

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 平衡了生产者和消费者的处理能力。(削峰填谷)

阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

服务器收到的来自于客户端/用户的请求,不是一成不变的。可能会因为一些突发事件,引起请求数目暴增~~

一台服务器,同一时刻能处理的请求数量是有上限,不同的服务器承担的上限是不一样的。

在一个分布式系统中,就经常会出现,有的机器能承担的压力更大,有的则更小~~

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


正因为生产者消费者模型,这么重要,虽然阻塞队列只是一个数据结构,但我们会把这个数据结构(阻塞队列)单独实现成一个服务器程序并且使用单独的主机/主机集群来部署

此时,这个所谓的则塞队列,就进化成了“消息队列”。

2.标准库中的阻塞队列

java标准库中已经提供了现成的阻塞队列的实现了,有基于链表、堆、优先级、数组实现的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Array 这个版本的速度要更快,前提是得先知道最多有多少元素。(对于 Array 版本来说,频繁扩容是一个不小的开销)

如果不知道有多少元素,使用 Linked 更合适。

对于BlockfingQueue来说,offer 和 poll 不带有阻塞功能,而 put 和 take 带有阻塞功能。

3) 拟实现阻塞队列

这里我们基于数组,循环队列来简单实现一个阻塞队列。

循环队列就像是把数组首尾连接起来,组成一个环状,以此提高使用效率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面简单实现阻塞队列的两个主要功能puttake

  • put:入队列。
  • take:出队列。

阻塞队列的构成:

  1. 循环队列

    • 用 size 来标记队列长度,可以判断队列是否为满。
    • 实现循环效果,头(head) 尾(hail) 相连。
  2. 实现阻塞

    • 入队列时。当队列满的时候,再进行 put 就会产生阻塞。
    • 出队列时。当队列空的时候,再进行 take 也会产生阻塞。
  3. 写完代码,需要考虑线程安全问题。(内存可见性、加锁)

    • 内存可见性
    • 加锁
  4. wait 搭配 while 循环使用

    原因:

    • 意外被 interrupted 唤醒。
    • 多线程下,出现线程安全问题。

下面是实现代码:

class MyBlockingQueue {private String[] items = new String[1000];volatile private int head = 0;volatile private int tail = 0;volatile private int size = 0;public void put (String elem) throws InterruptedException {synchronized (this) {while (size >= items.length) {//队列满,开始阻塞等待。System.out.println("队列满,开始阻塞等待!!!");this.wait();//return;}items[tail] = elem;tail ++;if (tail >= items.length) {tail = 0;}size ++;this.notify();}}public String take () throws InterruptedException {synchronized (this) {//判断队列是否为空,若为空则不能出队列。while (size == 0) {//队列空,开始阻塞等待System.out.println("队列空,开始阻塞等待!!");this.wait();//return null;}String elem = items[head];head ++;if (head >= items.length) {head = 0;}size --;//使用 notify 来唤醒队列满的阻塞情况。this.notify();return elem;}}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue();//生产者Thread t1 = new Thread(() ->{int count = 0;while (true) {try {queue.put(count + "");System.out.println("生产元素:" + count);count ++;//Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});//消费者Thread t2 = new Thread(() -> {while (true) {try {String count = queue.take();System.out.println("消费元素:" + count);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();}
}/*结果
先生产1000个元素,阻塞等待消费元素。
之后消费一个元素即生产一个元素。
*/

3、定时器

什么是定时器?

⏰ 定时器也是软件开发中的一个重要组件。类似于一个 “闹钟”。达到一个设定的时间之后,就执行某个指定好的代码

1) 标准库中的定时器
  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数。第一个参数 (TimerTask) 指定即将要执行的任务代码, 第二个参数 (delay) 指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("定是任务执行");}
}, 3000);System.out.println("程序开始运行");

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当我们运行代码,就会发现,打印完 hello 程序并没有结束,而是在持续执行

原因就在于:Timer 内部,有自己的线程。为了保证随时可以处理新安排的任务,这个线程会持续执行。并且这个线程还是个前台线程。

上述代码中,可以看到 schedule 有两个参数, 其中的一个参数 TimeTask 类似于 Runnable。在那里会记录时间,以及执行什么样的任务。

下图中的源代码 TimerTask 也实现了 Runnable 接口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


2) 模拟实现定时器

一个定时器里,是可以有很多个任务的!!

先要能够把一个任务给描述出来,再使用数据结构把多个任务组织起来。

定时器的构成思路:

  1. 创建一个TimerTask这样类,表示一个任务。这个任务,就需要包含两方面,任务的内容,任务的实际执行时间。

  2. 使用一定的数据结构,把多个 TimerTask 给组织起来。

    如果使用 List (数组,链表)组织 TimerTask 的话。在任务特别多的情况下,如何确定哪个任务,何时能够执行呢?这样就需要搞一个线程,不停的对上述的List进行遍历。看这里的每个元素,是否到了时间,时间到就执行,时间不到,就跳过扫描下一个。

    这个思路并不科学!!如果这些任务的时间都还为时尚早。在时间到达之前,此处的扫描线程就需要一刻不停的反复扫描。

    这里可以使用优先级队列,来组织所有的任务。因为队首元素,就是时间最小的任务

  3. 搞一个扫描线程,负责监控队首元素的任务是否时间到,如果到时间了,就执行 run 方法来完成任务。


注意问题:

  1. 代码中所使用的优先级队列不是线程安全的!!在此需要针对 queue 的操作,进行加锁。

  2. 扫描线程中(阻塞等待),直接使用 sleep 进行休眠,是否合适呢?? 非常不合适的!!!!

    • sleep 进入阻塞后,不会释放锁。影响其他线程去执行这里的 schedule.
    • sleep 在休眠的过程中,不方便提前中断。(虽然可以使用 interrupt 来中断。但是 interrupt 意味着线程应该要结束了)
    • 这里可以使用 wait 和 notify 来阻塞和唤醒程序。
      • 当队列为空时,需要阻塞等待。当确定了最早需要执行任务时,也需要阻塞等待(等待任务执行)
      • 当 queue 中添加新任务时,就需要唤醒正在阻塞等待的程序,来重新进行判定哪个是最新的任务。
  3. 随便写一个类,它的对象都能放到优先级队列中嘛?有没有什么特别的要求?

    要求放到优先级队列中的元素,是“可比较的"

    通过 Comparable 或者 Comparator 定义任务之间的比较规则。此处,我们在 MyTimerTask 这里,让他实现 Comparable。


下面是实现代码:

import java.util.PriorityQueue;//模拟实现定时器
class MyTimerTask implements Comparable<MyTimerTask>{//执行任务时间private long time;//执行任务内容private Runnable runnable;public MyTimerTask(Runnable runnable, long delay) {time = System.currentTimeMillis() + delay;this.runnable = runnable;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}@Overridepublic int compareTo(MyTimerTask o) {return (int)(o.time - this.time);}
}class MyTimer {//使用优先级队列private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();//定义锁对象private Object locker = new Object();public void schedule (Runnable runnable, long delay) {synchronized (locker) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);locker.notify();}}public MyTimer() {Thread t = new Thread(() -> {while (true) {synchronized (locker) {try {if (queue.isEmpty()) {locker.wait();}long curTime = System.currentTimeMillis();MyTimerTask task = queue.peek();if (curTime >= task.getTime()) {queue.poll();task.getRunnable().run();} else {locker.wait(task.getTime() - curTime);}} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t.start();}
}
public class Demo2 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 3");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 2");}},2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 1");}},1000);System.out.println("开始执行!!!");}
}

4、线程池

池(Poll) 是一个非常重要的思想方法。包括内存池、进程池、连接池、常量池……这些“池”概念上都是一样的。

线程池可以管理和重用多个线程,以提高应用程序的性能和资源利用率。线程池维护了一个线程队列,当需要执行任务时,可以从线程池中获取一个空闲线程来执行任务,而不需要每次都创建新的线程。这样可以减少线程创建和销毁的开销,并且可以限制并发线程的数量,避免资源耗尽

为啥,从池子里取,就比从系统这里创建线程更快更高效呢?

  1. 如果是从系统这里创建线程,需要调用系统api。由操作系统内核,来完成线程的创建过程。(这里的内核是给所有的进程来提供服务的,除了创建线程这个任务外,还有其他任务。因此效率低下)不可控的!!
  2. 如果是从线程池这里获取线程,上述的内核中进行的操作,都提前做好了,现在的取线程的过程,纯粹的用户代码完成(纯用户态)可控的!!

因此线程池最大的好处就是减少每次启动线程、销毁线程的损耗


1) 工厂模式

什么是工厂模式??

工厂模式是一种创建对象的设计模式,它通过将对象的创建委托给一个工厂类来解决对象创建的问题。工厂模式提供了一种灵活的方式来创建对象,使得客户端不需要使用new关键字来创建对象,而是通过调用工厂类的方法来获得所需的对象

说完概念后,再来简单描述一下工厂模式。

一般创建对象,都是通过 new ,通过构造方法。但是构造方法存在重大缺陷!!此时就可以使用工厂模式来解决问题了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用工厂模式来解决上述(无法重载)问题。不使用构造方法了。而是使用普通的方法来构造对象。这样的方法名字就可以是任意的了。普通方法内部,再去 new 对象。由于普通方法目的是为了创建出对象来,因此这样的方法一般得是静态的

//工厂模式
class Point {public static Point makePointXY(double x, double y) {  //工厂方法return new Point();}public static Point makePointRA(double r, double a) {  //工厂方法return new Point();}
}
public class Demo1 {public static void main(String[] args) {Point point = Point.makePointXY(5, 6);Point point = Point.makePointAR(7, 8);}
}

我们再来看一看下面源代码中是如何运用工厂模式的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在使用的时候,就直接去调用方法即可。

Executors:被称为“工厂类“。

newFixedThreadPool:被称为“工厂方法”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


2) 标准库中的线程池
  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService.
  • 线程池对象创建好后,可以通过 submit方法,把任务添加到线程池中.
ExecutorService executorService = Executors.newFixedThreadPool(4);for (int i = 0; i < 1000; i++) {executorService.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});
}
MethodsDescription
newCachedThreadPool()创建出一个线程数目动态变化的线程池.
newFixedThreadPool(int nThreads)创建一个固定线程数量的线程池.
newScheduledThreadPool(int corePoolSize)类似于定时器,在后续的某个时刻再执行。
newSingleThreadExecutor()包含单个线程(比原生创建的线程 api 更简单一点)

1.ThreadPoolExecutor类

除了上述这些线程池之外,标准库还提供了一个接口更丰富的线程池类。

为了方便使用,将上述的几个方法进一步封装,形成一个新的 ThreadPoolExecutor 类,以此更好的满足实际的需求。

我们可以在官方文档中查看 ThreadPoolExecutor 类,在 java.util.concurrent 这个包中,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


谈谈Java标准库里的线程池构造方法的参数和含义

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


当线程池无法接受新任务时,会调用一下方式来拒绝执行任务。

第一个拒绝策略

ThreadPoolExecutor.AbortPolicy

默认的拒绝策略,会抛出RejectedExecutionException异常

第二个拒绝策略

ThreadPoolExecutor.CallerRunsPolicy

会将任务回退给调用者线程来执行

第三个拒绝策略

ThreadPoolExecutor.DiscardOldestPolicy

会丢弃最早加入队列的任务,然后尝试重新添加新任务

第四个拒绝策略

ThreadPoolExecutor.DiscardPolicy

会直接丢弃该任务


上述谈到的线程池

  • 一组线程池,是封装过的Executors
  • 一组线程池,ThreadPoolExecutor 原生的

二者用哪个都可以,主要还是看实际需求。


3) 模拟实现线程池

线程池中所用到的数据结构还是阻塞队列。

实现其主要功能 submit.

下面是代码实现:

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;//简单模拟实现 线程池
class MyPollThread {static BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();public void submit (Runnable runnable) throws InterruptedException {queue.put(runnable);}public static MyPollThread newMyPollThread(int n) {for (int i = 0; i < n; i++) {Thread t = new Thread(()-> {while (true) {try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}return new MyPollThread();}
}public class Demo2 {public static void main(String[] args) throws InterruptedException {MyPollThread myPollThread = MyPollThread.newMyPollThread(4);for (int i = 0; i < 1000; i++) {myPollThread.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " hello");}});}}
}

关于线程池还有一个问题,在创建线程池的时候,线程个数是咋来的呢??

不同的项目中,线程要做的工作,是不一样的~~

  • 有的线程的工作,是"CPU密集型",线程的工作全是运算

    大部分工作都是要在CPU上完成的.
    CPU得给他安排核心去完成工作才可以有进展~~

    如果CPU是N个核心,当你线程数量也是N的时候

    理想情况下,每个核心上一个线程.
    如果搞很多的线程,线程也就是在排队等待,不会有新的进展.

  • 有的线程的工作,是"IO密集型",读写文件,等待用户输入,网络通信

    涉及到大量的等待时间。等的过程中,没有使用 cpu 这样的线程就算更多一些,也不会给CPU造成太大的负担。

    比如CPU是16个核心,写32个线程。

    由于是IO密集的,这里的大部分线程都在等,都不消耗CPU,反而CPU的占用情况还很低~~

实际开发中,一个线程往往是一部分工作是cpu密集的,一部分工作是io密集的。此时,一个线程,几成是在cpu上运行,几成是在等待io,这说不好!
这里更好的做法,是通过实验的方式,来找到合适的线程数。通过性能测试,尝试不同的线程数目。实验过程中,找到性能和系统资源开销比较均衡的数值。

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

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

相关文章

用向量数据库Milvus Cloud搭建检索知识库机器人

检索知识库 Milvus 中已经存储了文本块向量,现在可以进行向量查询了。 以下函数创建了 1 个查询 pipeline。注意,这是本教程中最为关键的一个步骤! ops.ann_search.osschat_milvus(host=MILVUS_HOST, port=MILVUS_PORT, **{metric_type: IP, limit: 3, output_fields: [text…

全新UI彩虹外链网盘系统源码(前后端美化模板)

全新UI彩虹外链网盘系统源码前后端美化模板&#xff0c;支持所有格式文件的上传、生成文件外链、图片外链、音乐视频外链等功能&#xff0c;同时还可以自动生成相应的 UBB 代码和 HTML 代码&#xff0c;支持文本、图片、音乐、视频在线预览。这不仅仅是一个网盘&#xff0c;更是…

redis的简单使用

文章目录 环境安装与配置redis发布-订阅相关命令redis发布-订阅的客户端编程redis的订阅发布的例子 环境安装与配置 sudo apt-get install redis-server # ubuntu命令安装redis服务ubuntu通过上面命令安装完redis&#xff0c;会自动启动redis服务&#xff0c;通过ps命令确认&a…

【Linux】进程控制基础知识

目录 一&#xff0c;fack回顾 二&#xff0c;进程终止 1.进程终止&#xff0c;操作系统做了什么&#xff1f; 2.进程终止&#xff0c;常见的方式 1.main函数的&#xff0c;return 返回码 2. exit()函数 三&#xff0c;进程等待 1. 回收进程方法 &#xff08;1. wai…

【单片机】16-LCD1602和12864显示器

1.LCD显示器相关背景 1.LCD简介 &#xff08;1&#xff09;显示器&#xff0c;常见显示器&#xff1a;电视&#xff0c;电脑 &#xff08;2&#xff09;LCD&#xff08;Liquid Crystal Display&#xff09;&#xff0c;液晶显示器&#xff0c;原理介绍 &#xff08;3&#xff…

国庆10.03

运算符重载 代码 #include <iostream> using namespace std; class Num { private:int num1; //实部int num2; //虚部 public:Num(){}; //无参构造Num(int n1,int n2):num1(n1),num2(n2){}; //有参构造~Num(){}; //析构函数const Num operator(const Num &other)co…

【计算机网络笔记十】计算机网络面试问题总结

1. 计算机网络的各层协议及作用&#xff1f; 计算机网络体系可以大致分为一下三种&#xff0c;OSI 七层模型、TCP/IP 四层模型和五层模型。 OSI 七层模型&#xff1a;大而全&#xff0c;但是比较复杂、而且是先有了理论模型&#xff0c;没有实际应用。TCP/IP 四层模型&#x…

Java之SpringCloud Alibaba【六】【Alibaba微服务分布式事务组件—Seata】

一、事务简介 事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。 在关系数据库中&#xff0c;一个事务由一组SQL语句组成。 事务应该具有4个属性: 原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。 原子性(atomicity) ∶个事务…

【Leetcode】 131. 分割回文串

给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 示例 1&#xff1a; 输入&#xff1a;s "aab" 输出&#xff1a;[["a","a"…

Leetcode 662. 二叉树最大宽度

文章目录 题目代码&#xff08;9.30 首刷看解析&#xff09; 题目 Leetcode 662. 二叉树最大宽度 代码&#xff08;9.30 首刷看解析&#xff09; class Solution { public:int widthOfBinaryTree(TreeNode* root) {unsigned long long res 1;using pr pair<TreeNode*, u…

Doctest:让你的测试更简单高效

简介&#xff1a;Doctest 是 Python 标准库的一部分&#xff0c;它允许开发者通过在文档字符串&#xff08;docstrings&#xff09;中编写示例来进行测试。这不仅可以在确保代码正确性的同时编写文档&#xff0c;还可以让读者更容易理解代码的用法和期望的输出。 历史攻略&…

【源码】hamcrest 源码阅读及空对象模式、模板方法模式的应用

文章目录 前言1. 类图概览2. 源码阅读2.1 抽象类 BaseMatcher2.1 接口 Description提炼模式&#xff1a;空对象模式 2. 接口 Description 与 SelfDescribing 配合使用提炼模式 模板方法 后记 前言 hamcrest &#xff0c;一个被多个测试框架依赖的包。听说 hamcrest 的源码质量…

RabbitMQ学习笔记(消息发布确认,死信队列,集群,交换机,持久化,生产者、消费者)

MQ&#xff08;message queue&#xff09;&#xff1a;本质上是个队列&#xff0c;遵循FIFO原则&#xff0c;队列中存放的是message&#xff0c;是一种跨进程的通信机制&#xff0c;用于上下游传递消息。MQ提供“逻辑解耦物理解耦”的消息通信服务。使用了MQ之后消息发送上游只…

利用Qt实现可视化科学计算器

&#x1f4de;个人信息 学号&#xff1a;102101433 姓名&#xff1a;林堂钦 &#x1f4a1; 作业基本信息 【课程】福州大学2021级软件工程Ahttps://bbs.csdn.net/forums/ssynkqtd-05作业要求链接https://bbs.csdn.net/topics/617294583作业目标 实现一个简易计算器&…

最短路径专题2 最短距离-多终点(堆优化版)

题目&#xff1a;样例&#xff1a; 输入 6 6 0 0 1 2 0 2 5 0 3 1 2 3 2 1 2 1 4 5 1 输出 0 2 3 1 -1 -1 思路&#xff1a; 根据题意&#xff0c;数据范围也小&#xff0c;也可以用朴素版的Dijsktra来做&#xff0c;朴素版的Dijsktra我做过了一遍了&#xff0c;可以看以一下我…

MySQL - mysql服务基本操作以及基本SQL语句与函数

文章目录 操作mysql客户端与 mysql 服务之间的小九九了解 mysql 基本 SQL 语句语法书写规范SQL分类DDL库表查增 mysql数据类型数值类型字符类型日期类型 示例修改&#xff08;表操作&#xff09; DML添加数据删除数据修改数据 DQL查询多个字段条件查询聚合函数分组查询排序查询…

【数据科学】Scikit-learn[Scikit-learn、加载数据、训练集与测试集数据、创建模型、模型拟合、拟合数据与模型、评估模型性能、模型调整]

这里写目录标题 一、Scikit-learn二、加载数据三、训练集与测试集数据四、创建模型4.1 有监督学习评估器4.1.1 线性回归4.1.2 支持向量机(SVM)4.1.3 朴素贝叶斯4.1.4 KNN 4.2 无监督学习评估器4.2.1 主成分分析(PCA)4.2.2 K Means 五、模型拟合5.1 有监督学习5.2 无监督学习 六…

React18入门(第一篇)——JSX、TSX语法详解

文章目录 一、JSX 语法简介二、和 HTML 标签的几点不同三、JSX 属性四、JSX 事件4.1 简单点击事件4.2 类型限制4.3 带参数&#xff0c;箭头函数 五、插入 JS 变量六、JSX 中使用条件判断七、循环 一、JSX 语法简介 JSX - 是 JS 的扩展&#xff0c;写在 JS 代码里面&#xff0c…

STM32 DMA从存储器发送数据到串口

1.任务描述 &#xff08;1&#xff09;ds18b20测量环境温度存储到存储器&#xff08;数组&#xff09;中。 &#xff08;2&#xff09;开启DMA将数组中的内容&#xff0c;通过DMA发送到串口 存在问题&#xff0c;ds18b20读到的数据是正常的&#xff0c;但是串口只是发送其低…

WSL安装异常:WslRegisterDistribution failed with error: 0xc03a001a

简介&#xff1a;如果文件夹右上角是否都有两个相对的蓝色箭头&#xff0c;在进行安装wsl时&#xff0c;设置就会抛出 Installing WslRegisterDistribution failed with error: 0xc03a001a的异常 历史攻略&#xff1a; 卸载WSL WSL&#xff1a;运行Linux文件 WSL&#xff1…