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

news2025/1/13 13:25:05

文章目录

  • 多线程案例
      • 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() {
    @Override
    public 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;
    }

    @Override
    public 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() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        },3000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        },2000);

        myTimer.schedule(new Runnable() {
            @Override
            public 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() {
        @Override
        public 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() {
                @Override
                public 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.coloradmin.cn/o/1058049.html

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

相关文章

大屏自适应容器组件-Vue3+TS

1.引言 在做数字大屏时&#xff0c;图表能跟着浏览器的尺寸自动变化&#xff0c;本文采用Vue3前端框架&#xff0c;采用TypeScript语言&#xff0c;封装了一个大屏自适应组件&#xff0c;将需要显示的图表放入组件的插槽中&#xff0c;就能实现自适应屏幕大小的效果。 2.实际…

mysql-binlog

1. 常用的binlog日志操作命令 1. 查看bin-log是否开启 show variables like log_%;2. 查看所有binlog日志列表 show master logs;3.查看master状态 show master status;4. 重置&#xff08;清空&#xff09;所有binlog日志 reset master;2. 查看binlog日志内容 1、使用mysqlb…

前端相关题目随笔

Vh虽然获取到了视口高度&#xff0c;但是vh会随着屏幕的、大小变化&#xff0c;所以当减去一个数字之后&#xff0c;就会显示错误。 生成id 如果没有设置id&#xff0c;则可以通过new Date.getTime()获取一个时间&#xff0c;作为一个单独的id&#xff0c;也可以通过下载uuid生…

竞赛选题 机器视觉人体跌倒检测系统 - opencv python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 机器视觉人体跌倒检测系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&…

想要精通算法和SQL的成长之路 - 二叉树的序列化和反序列化问题

想要精通算法和SQL的成长之路 - 二叉树的序列化和反序列化问题 前言一. 二叉树的层序遍历&#xff08;BFS&#xff09;二. 二叉树的序列化与反序列化2.1 序列化操作2.2 反序列化操作 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 二叉树的层序遍历&#xff08;BFS&#xf…

Redis学习笔记(下):持久化RDB、AOF+主从复制(薪火相传,反客为主,一主多从,哨兵模式)+Redis集群

十一、持久化RDB和AOF 持久化&#xff1a;将数据存入硬盘 11.1 RDB&#xff08;Redis Database&#xff09; RDB&#xff1a;在指定的时间间隔内将内存中的数据集快照写入磁盘&#xff0c;也就是行话讲的Snapshot快照&#xff0c;它恢复时是将快照文件直接读到内存里。 备份…

操作系统原理-习题汇总

临近毕业&#xff0c;整理一下过去各科习题及资料等&#xff0c;以下为操作系统原理的习题汇总&#xff0c;若需要查找题目&#xff0c;推荐CtrlF或commandF进行全篇快捷查找。 操作系统原理 作业第一次作业选择题简答题 第二次作业选择题简答题 第三次作业选择题简答题 第四次…

acwing215.破译密码题解(容斥原理+mobius函数)

达达正在破解一段密码&#xff0c;他需要回答很多类似的问题&#xff1a; 对于给定的整数 a,b 和 d&#xff0c;有多少正整数对 x,y&#xff0c;满足 x≤a&#xff0c;y≤b&#xff0c;并且 gcd(x,y)d. 作为达达的同学&#xff0c;达达希望得到你的帮助。 输入格式 第一行包…

H5生成二维码

H5生成二维码&#xff1a; 1.引入js库&#xff0c;可自行点击链接复制使用 <script type"text/javascript" src"http://static.runoob.com/assets/qrcode/qrcode.min.js"></script>2.加入二维码占位区HTML <div id"qrCode">…

初识Java 12-2 流

目录 中间操作 跟踪与调试 对流元素进行排序 移除元素 将函数应用于每个流元素 在应用map()期间组合流 Optional类型 便捷函数 创建Optional Optional对象上的操作 由Optional组成的流 本笔记参考自&#xff1a; 《On Java 中文版》 中间操作 ||| 中间操作&#xf…

Linux嵌入式学习之Ubuntu入门(六)shell脚本详解

系列文章内容 Linux嵌入式学习之Ubuntu入门&#xff08;一&#xff09;基本命令、软件安装、文件结构、编辑器介绍 Linux嵌入式学习之Ubuntu入门&#xff08;二&#xff09;磁盘文件介绍及分区、格式化等 Linux嵌入式学习之Ubuntu入门&#xff08;三&#xff09;用户、用户组…

从0手写两轮差速机器人urdf模型

文章目录 前言一、基本理论二、实现步骤1.创建一个机器人建模功能包2.使用圆柱体创建一个车体模型2.同理创建机器人其它构件3.机器人模型添加传感器 前言 最近为找到与自己课题应用场景相适应的机器人结构&#xff0c;对机器人建模方面的内容进行了了解和学习&#xff0c;计划…

博途SCL区间搜索指令(判断某个数属于某个区间)

S型速度曲线行车位置控制,停靠位置搜索功能会用到区间搜索指令,下面我们详细介绍区间搜索指令的相关应用。 S型加减速行车位置控制(支持点动和停车位置搜索)-CSDN博客S型加减速位置控制详细算法和应用场景介绍,请查看下面文章博客。本篇文章不再赘述,这里主要介绍点动动和…

【nginx】Nginx配置:

文章目录 一、什么是Nginx&#xff1a;二、为什么使用Nginx&#xff1a;三、如何处理请求&#xff1a;四、什么是正向代理和反向代理&#xff1a;五、nginx 启动和关闭&#xff1a;六、目录结构&#xff1a;七、配置文件nginx.conf&#xff1a;八、location&#xff1a;九、单页…

嵌入式C 语言函数宏封装妙招

1. 函数宏介绍 函数宏&#xff0c;即包含多条语句的宏定义&#xff0c;其通常为某一被频繁调用的功能的语句封装&#xff0c;且不想通过函数方式封装来降低额外的弹栈压栈开销。 函数宏本质上为宏&#xff0c;可以直接进行定义&#xff0c;例如&#xff1a; #define INT_SWA…

Spring的注解开发-注解方式整合MyBatis代码实现

之前使用xml方式整合了MyBatis&#xff0c;文章导航&#xff1a;Spring整合第三方框架-MyBatis整合Spring实现-CSDN博客 现在使用注解的方式无非是就是将xml标签替换为注解&#xff0c;将xml配置文件替换为配置类而已。 非自定义配置类 package com.example.Configure;import c…

嵌入式系统中如何正确使用动态内存?

​ 大家好&#xff0c;今天给大家分享一下&#xff0c;动态内存的使用方法 一&#xff0e; 常见错误与预防 1. 分配后忘记释放内存 void func(void) {p malloc(len);do_something(p);return; /*错误&#xff01;退出程序时没有释放内存*/ }预防&#xff1a; 编写代码…

DevExpress ChartControl 画间断线

效果如下&#xff1a; 解决办法&#xff1a;数据源间断位置加入double.NaN demo下载

Linux 下如何调试代码

debug 和 release 在Linux下的默认模式是什么&#xff1f; 是release模式 那你怎么证明他就是release版本? 我们知道如果一个程序可以被调试&#xff0c;那么它一定是debug版本&#xff0c;如果它是release版本&#xff0c;它是没法被调试的&#xff0c;所以说我们可以来调试一…

基于SpringBoot+MyBatis实现的个人博客系统(一)

这篇主要讲解一下如何基于SpringBoot和MyBatis技术实现一个简易的博客系统(前端页面主要是利用CSS,HTML进行布局书写),前端的静态页面代码可以直接复制粘贴,后端的接口以及前端发送的Ajax请求需要自己书写. 博客系统需要完成的接口: 注册登录博客列表页展示博客详情页展示发布博…