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

文章目录

  • 多线程案例
      • 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。

多线程基础篇(多线程案例)_第1张图片

此时,我们要是去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)??

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

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

多线程基础篇(多线程案例)_第2张图片

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

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

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


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

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

先考虑一种加锁方式:

多线程基础篇(多线程案例)_第3张图片

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

多线程基础篇(多线程案例)_第4张图片

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

多线程基础篇(多线程案例)_第5张图片
多线程基础篇(多线程案例)_第6张图片

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

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

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


多线程基础篇(多线程案例)_第7张图片

思考:后续每次调用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 引用。

多线程基础篇(多线程案例)_第8张图片

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

2、阻塞队列

1) 阻塞队列是什么?

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

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

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

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


2) 生产者消费者模型

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

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

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

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

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

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

    多线程基础篇(多线程案例)_第9张图片

    多线程基础篇(多线程案例)_第10张图片

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

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

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

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

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

多线程基础篇(多线程案例)_第11张图片

多线程基础篇(多线程案例)_第12张图片


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

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

2.标准库中的阻塞队列

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

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

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

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

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

3) 拟实现阻塞队列

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

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

多线程基础篇(多线程案例)_第13张图片

下面简单实现阻塞队列的两个主要功能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("程序开始运行");

多线程基础篇(多线程案例)_第14张图片

当我们运行代码,就会发现,打印完 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 ,通过构造方法。但是构造方法存在重大缺陷!!此时就可以使用工厂模式来解决问题了。

多线程基础篇(多线程案例)_第15张图片

使用工厂模式来解决上述(无法重载)问题。不使用构造方法了。而是使用普通的方法来构造对象。这样的方法名字就可以是任意的了。普通方法内部,再去 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);
    }
}

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

多线程基础篇(多线程案例)_第16张图片

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

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");
        }
    });
}
Methods Description
newCachedThreadPool() 创建出一个线程数目动态变化的线程池.
newFixedThreadPool(int nThreads) 创建一个固定线程数量的线程池.
newScheduledThreadPool(int corePoolSize) 类似于定时器,在后续的某个时刻再执行。
newSingleThreadExecutor() 包含单个线程(比原生创建的线程 api 更简单一点)

1.ThreadPoolExecutor类

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

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

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

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


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

多线程基础篇(多线程案例)_第17张图片


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

第一个拒绝策略

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,这说不好!
这里更好的做法,是通过实验的方式,来找到合适的线程数。通过性能测试,尝试不同的线程数目。实验过程中,找到性能和系统资源开销比较均衡的数值。

你可能感兴趣的:(JavaEE,多线程,简单工厂模式,单例模式,线程池,定时器,阻塞队列)