Java——多线程案例

news2024/11/29 18:02:17

目录

一、单例模式

1.饿汉模式

 2.懒汉模式

3.线程安全问题

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

二、阻塞式队列

1.什么是阻塞队列

2.生产者消费模型

 生产者消费者模型意义:

1.解耦合

2.削峰填谷

3.标准库中的阻塞队列

三、定时器

1.定时器是什么

2.标准库中的定时器

 3.模拟实现简单的定时器

四、线程池

1.线程池是什么

2.实现线程池


一、单例模式

啥是单例模式?

单例模式是一种设计模式,设计模式好比围棋中的棋谱,通过棋谱的套路来走,在应招的时候不会吃亏。

在软件开发中也有很多常见的“问题场景”,针对这些问题场景,大佬们总结了一些固定的套路。按照这个套路来实现代码也不会吃亏。                

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

这一点在很多场景上都需要,比如JDBC中的DataSource实例就只需要一个。

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

1.饿汉模式

class Singleton{
    private static  Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

说明:

  • 饿汉模式体现的饿汉体现在类加载的时候就创建了实例,比较急切。
  • instance是类的静态成员,在类Singleton被加载的时候,就会执行到这个的创建实例操作。
  • 通过getInstance这个方法来获取到这个类的实例,后续如果想使用这个类的实例,都通过getInstance方法来获取。
  • 构造方法设置成私有(private),此时外部也无法通过new来创建对象了,这样就保证了只能有一个对象。

 2.懒汉模式

懒汉模式的体现:类加载的时候不创建实例。第一次使用的时候才创建实例。

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

说明:

  • 首次调用getInstance()才会真正去创建出实例。
  • 通过利用if语句保证了实例创建的唯一性。

3.线程安全问题

对于上面两种写法,饿汉模式在多线程情况下是安全的,而懒汉模式在多线程情况下是有安全问题的。

原因:

  • 多个线程同时修改一个变量,此时就可能出现线程安全问题。
  • 在饿汉模式中,getInstance()只是进行读取,并没有进行修改。
  • 而懒汉模式中,既会读取,又会修改,就可能存在问题。

在懒汉模式中,线程安全问题发生在首次创建实例时,如果多个线程中同时调用getInstance方法,就可能导致创建出多个实例。

一旦实例已经创建好了,后面在多线程环境调用getInstance就不再有线程安全问题了(第一次已经创建了,后面就不会再进入if语句了)。

如图:

可见懒汉模式在多线程的情况下存在线程安全问题。

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

加上synchronized:

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

通过加上synchronized保证了线程的安全,但是一旦代码这么写,后续多个线程每次调用getInstance都需要先加锁,然而加锁是一个开销很大的操作,加锁就可能涉及到锁冲突,一冲突就会引起阻塞等待。

针对上面的问题对代码的高效率执行进行改进:

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

通过利用双重if语句,保证了在已经创建了实例之后,多个线程再调用getInstance时避免了再次加锁的情况。

说明:Singletonlazy加上volatile是为了避免指令重排序和内存可见性导致的问题。

比如:

内存可见性问题:线程A创建完对象,但未把工作内存中的内容同步入内存中,导致B线程进入第一个if语句,B要等到A线程释放锁后才进入同步块中,而A释放锁把内容放到内存中,B进入同步块后,他就能刷新读取到内存中对象instance的最新状态,进入if语句又不满足,这样子反而降低了效率。

指令重排序问题:线程A未完全创建好对象,即分配了内存空间但未有内容,然后指令重排序后,线程B执行到第一个if语句,不为空,然后返回得到一个未完全创建好的对象。


二、阻塞式队列

1.什么是阻塞队列

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

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

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

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

2.生产者消费模型

何为生产者消费模型?

  • 比如一个产业链,有生产部,销售部和仓库,生产部不断生产产品然后把产品放到仓库中进行存储,然后销售部就不断地从仓库里取货来进行售卖。而仓库就是相当于阻塞队列。
  • 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通信。所以生产者生产完数据之后不用等待消费者处理,直接扔到阻塞队列中,消费者不找生产者要数据,而是直接从阻塞队列里取。

 生产者消费者模型意义:

1.解耦合

耦合度:两个模块,联系越紧密,耦合度越高

比如一个简单的分布式系统:

 说明:

如果A和B直接交互(A把请求发送给B,B把响应返回给A)。

彼此之间的耦合就是比较高的:

  1. 如果B出现问题,但可能就把A也影响到了。
  2. 如果未来再添加一个C,就需要对A这边的代码做出一定的改动。

相比之下,使用生产者消费者模型,就可以有效的解决这种耦合问题。

 此时这种情况耦合就会降低,如果B这边出现问题,就不会对A产生直接的影响(A只是和队列交互,不知道B的存在)。后续如果新增一个C,此时A不必进行任何修改,只需要让C从队列中获取数据即可。

2.削峰填谷

削峰:

在大量购物的情景下,服务器同一时刻可能会收到客户端大量的支付请求,如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求。

这样做可以有效进行“削峰”,防止服务器被突然的一波请求直接冲垮。

填谷:

在系统负载较低时,合理处理任务,以平衡整体资源使用,提高系统性能和资源使用效率。

3.标准库中的阻塞队列

在java标准库中内置了阻塞队列。如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可。

  • BlockingQueue是一个接口,真正实现的类是LinkedBlockQueue。

  • put方法用于阻塞式的入队列,take用于阻塞式的出队列 。

说明:

put:当队列已满时,它会阻塞调用线程直到队列中有空间可用。

take:如果队列为空,调用此方法的线程将会被阻塞,直到有新的元素可用,确保消费者在没有数据可处理时不会执行无效操作。

  • BlockingQueue也有offer,poll,peek等方法,但是这些方法不带有阻塞特性。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
//入队列
queue.put("abc");
//出队列,如果没有put而直接take就会阻塞
String elem = queue.take();

 生产者消费者模型:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        Thread customer = new Thread(()->{
            while(true){
                try{
                    int value = blockingDeque.take();
                    System.out.println("消费元素:"+value);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"消费者");
        customer.start();
        Thread producer = new Thread(()->{
            Random random = new Random();
            while(true){
                try{
                    int num = random.nextInt(1000);
                    System.out.println("生产元素:"+num);
                    blockingDeque.put(num);
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"生产者");
        producer.start();
        customer.join();
        producer.join();
    }
}

运行结果:

 上述结果中,阻塞队列中一直生产一直消费,不断地输出值。可以通过设置sleep的值来控制生产和消费的速度。

阻塞队列的实现

  • 通过“循环队列”的方式来实现。
  • 使用synchronized进行加锁控制。
  • put插入元素的时候,判定如果队列满了,就进行wait(注意,要在循环中进行wait,被唤醒时不一定队列就不满了,因为同时可能是唤醒了多个线程,然后多个线程对同一把锁进行争抢)。
  • take取出队列的时候,判定如果队列为空,就进行wait。(也是循环wait)
public class ModelTest {
    private int[] BlockQueue = new int[1000];
    private volatile int head = 0;
    private volatile int tail = 0;
    private volatile int size = 0;

    public int getSize(){
        return size;
    }

    public void put(int elem ) throws InterruptedException {
        synchronized (this) {
            /*if(size == BlockQueue.length){
               this.wait();
            }
*/
            //此处最好用while语句,否则当减少一个元素的时候,notifyAll唤醒
            //所有等待的线程去竞争锁,当锁被抢占的时候,可能队列就已经满了
            //如果用了if而不用while,所有被唤醒的线程继续往下执行产生线程
            //安全问题,而如果用了while,抢不到锁的线程发现又满了,要继续等待
            //下面的take函数也同理
            while(size ==BlockQueue.length){
                this.wait();
            }
            BlockQueue[tail] = elem;
            tail = (tail + 1) % BlockQueue.length;
            size++;
            this.notifyAll();
        }
    }

    public int take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while(size == 0 ){
                this.wait();
            }
            ret = BlockQueue[head];
            head = (head + 1) % BlockQueue.length;
            size--;
            this.notifyAll();
        }
        return ret;
    }

    public static void main(String[] args) throws InterruptedException {
        ModelTest BlockQueue = new ModelTest();

        //创建多个消费者
        Thread customer1 = new Thread(()->{
            while(true){
                try{
                    int value = BlockQueue.take();
                    System.out.println(value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者1");
        Thread customer2 = new Thread(()->{
            while(true){
                try{
                    int value = BlockQueue.take();
                    System.out.println(value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者2");
        Thread customer3 = new Thread(()->{
            while(true){
                try{
                    Thread.sleep(1000);
                    int value = BlockQueue.take();
                    System.out.println(value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者3");
        Thread producer = new Thread(()->{
            Random random =new Random();
            while(true){
                try{
                    BlockQueue.put(random.nextInt(10000));
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"生产者");
        producer.start();
        customer2.start();
        customer1.start();
        customer3.start();

        producer.join();
        customer2.join();
        customer1.join();
        customer3.join();
    }
}

运行结果:

 说明:后续的代码中,有的要进行读,有的要进行修改,为了避免内存可见性问题,把volatile加上这样才能让变量能够及时的变化能够及时同步。


三、定时器

1.定时器是什么

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

定时器是一种实际开发中非常常用的组件。

比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连。

比如一个Map,希望里面的某个key在3s之后过期(自动删除)。

2.标准库中的定时器

  • 标准库中提供了一个Timer类。Timer类的核心方法为schedule。
  • schedule包含两个参数。第一个参数指定即将要执行的任务代码第二个参数指定多长时间之后执行(单位为毫秒)。

 代码示例:

public class ThreadDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println();
            }
        },300);
    }
}

运行结果:

说明:

  • Timer和TimerTask是用于在后台线程中调度任务的java.util类。简答地说,TimeTask是要执行的任务,Timer是调度器;
  • 此处的TimeTask是一个抽象类,实现了Runnable;
  • 此处使用匿名内部类的写法,继承了TimerTask并且创建出了一个实例;
  • 这里并不能用lambda表达式,因为要满足函数式接口,但是TimerTask是抽象类;
  • 此处匿名内部类中是重写了TimerTask继承Runnable中的run方法,通过run方法描述任务的详细情况。

 上述代码中,主线程执行schedule方法的时候,就是把要执行的任务(打印11)放到timer对象中了,而timer里头也包含了一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了(打印11)。

从运行窗口中可以发现,整个线程其实没有结束!!就是因为Timer内部的线程阻止了进程的结束。

此外,在Timer对象里是可以安排多个任务的。

public class ThreadDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println(3000);
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(2000);
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(1000);
            }
        },1000);
        System.out.println("程序启动!");
    }
}

运行结果:

 3.模拟实现简单的定时器

 定时器的构成:

  • 一个带优先级的阻塞队列

为何要带上优先级呢?

因为阻塞队列中的任意都有各自的执行时刻(delay)。最先执行的任务一定是delay(延时)最小的,使用带优先级的队列就可以高效的把这个delay最小的任务找出来。

  •  队列中的每个元素是一个Task对象。
  • Task中带有一个时间属性,队首元素就是即将要执行的任务。
  • 同时有一个worker线程一直扫描队首元素,看队首元素是否需要执行。

简易的表示:

   代码示例(利用优先级队列来实现的):

//创建一个类,描述一个任务
    //该对象要放到优先队列中,因此需要实现Comparable接口
    class MyTimerTask implements Comparable<MyTimerTask>{
        //让MyTimeTask实现比较器的函数,以时间为比较依据

    //要有一个执行的任务
    private Runnable runnable;
    //还要有一个执行任务的时间
    private long time;

    //构造方法,delay就是schedule方法传入的“相对时间”
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis()+delay;
    }

    //比较规则
    public int compareTo(MyTimerTask o){
        //当this.time小于o.time 将返回一个负数this.time应排在o.time之前
        //当返回大于零时,this排在o之后
        return (int)(this.time-o.time);
        //如果是想让队首元素是最大时间的值
        //return o.time-this.time;
    }

    public long getTime(){
        return time;
    }
    public Runnable getRunnable(){
        return runnable;
    }
}

//设置自己的定时器即自己的Timer类,通过队列来组织若干个TimerTask对象
class  MyTimer {
    //使用优先级队列或优先级阻塞队列的数据结构来存储要执行的任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //使用这个对象作为锁对象,借助该对象的wait/notify来解决while(true)的忙等问题,
    //即不要让该任务一直
    private Object locker = new Object();


    //通过schedule来往队列中中插入一个个TimerTask对象
    public void schedule(Runnable runnable,long delay){
        //需要用synchronized在offer的情况下保证线程安全
        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的目的是为了在wait被唤醒的时候,再次确认一下条件
                         while(queue.isEmpty()){
                             //使用wait进行等待
                             //这里的wait需要由另外的线程唤醒
                             locker.wait();
                         }
                        MyTimerTask task = queue.peek();
                         //比价一下看当前元素是否可以执行了
                         long curTime = System.currentTimeMillis();
                         if(curTime>= task.getTime()){
                             //如果当前时间已经到达了执行任务的时间,就可以执行任务了
                             //执行被重写的run方法
                             task.getRunnable().run();
                             //当任务执行完了,就可以从队列中删除了
                             queue.poll();
                         }else {
                             //当前时间还没有到任务时间,暂时不执行任务
                             //暂时先啥都不干,等待下一轮被唤醒判定是否到任务时间
                             locker.wait(task.getTime()-curTime);
                         }
                     }
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
            }
        });
        //启动线程
        t.start();
    }

}


//执行多个任务
public class ThreadDemo {
    public static void main(String[] args) {
        MyTimer timer =new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        },1000);
        System.out.println("程序开始执行");
    }
}

说明:

  • 该代码的执行逻辑主要是创建好了Timer对象的时候t线程就已经开始启动了,但是队列是空的状态,需要通过调用schedule函数把任务放入队列中然后执行notify来唤醒线程t,后面这个代码的执行逻辑基本上都是这样的。
  • 扫描线程扫描时间到了的任务,所安排的任务都是通过实现了Runnable的run方法来执行的。
  • synchronized的作用在于能够保证线程的安全,不会有其他线程同时去访问这个队列;此外synchronized还能够通过利用wait和notify管理和控制线程的执行情况,避免资源的浪费等。
  • 这个使用的是PriorityQueue,而不是PriorityBlockingQueue,其实就是因为要处理两个wait的地方,但是下面的优先级阻塞队列因为已经实现了空的情况下阻塞的功能,所以就不需要再加wait了。

 代码示例(利用优先级阻塞队列来实现的):

class TimerTask implements Comparable<TimerTask>{
    private Runnable runnable;
    private long time;

    public TimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = delay+System.currentTimeMillis();
    }
    public long getTime() {
        return time;
    }
    public Runnable getRunnable() {
        return runnable;
    }
    @Override
    public int compareTo(TimerTask o) {

        //谁时间小谁排前面
        return (int)(this.time - o.time);
    }
}
class MyTimer{
    //利用阻塞队列实现
    PriorityBlockingQueue<TimerTask> priorityBlockingQueue = new PriorityBlockingQueue<>();
    Object locker =new Object();
    public void Myschedule(Runnable runnable,long delay){
        priorityBlockingQueue.offer(new TimerTask(runnable,delay));
        synchronized (locker){
            locker.notify();
        }
    }
    public MyTimer(){
        Thread t = new Thread(()->{

            while(true) {
                try {
                    //利用阻塞队列的take方法,空队时不会取,会进行阻塞
                    TimerTask task = priorityBlockingQueue.take();
                    long curTime = System.currentTimeMillis();
                /*    long curTime = System.currentTimeMillis();
                    long TerminalTime = task.getDelay() + curTime;
                    这是一个错误代码,不能用变化的时间得到最后执行的时间,,因为时间一直是变化的,
                    这样做条件一直都会成立
                 */

                    if (curTime >= task.getTime()) {
                        //执行该任务
                        task.getRunnable().run();
                    } else {
                        //发现时间还没有到,再把任务放回去,阻塞队列的属性,满队时会阻塞不会放
                        priorityBlockingQueue.put(task);
                        synchronized (locker){
                            locker.wait(task.getTime()-curTime);
                        }
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                    break;
                }

            }
        });
        t.start();
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyTimer myTimer =new MyTimer();
        myTimer.Myschedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(3000);
            }
        },3000);
        myTimer.Myschedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(2000);
            }
        },2000);
        myTimer.Myschedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(1000);
            }
        },1000);
        System.out.println("程序启动!");
    }
}

四、线程池

1.线程池是什么

线程本身很轻量,但是如果进一步提高创建销毁的效率,线程的开销也不能忽视!

而线程池这个方案能够使线程创建和销毁的效率有所提高。

想象这么一个场景:

一个农家乐,做一份白切鸡的菜,每次都需要到镇上去买鸡然后杀掉做成白切鸡,老板觉得很麻烦,于是就在镇上一次性买了很多鸡回来,把鸡放到后院养着,这样就不用每次做白切鸡的时候都从镇上买鸡了,只需要从后院抓鸡就行了。
线程池也是如此, 后续如果想使用新的线程,不必重新创建了,直接从线程池拿过来就能用了,此时创建线程的开销就降低了。

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

为啥从线程池取的效率比新创建线程效率更高???

创建新的线程这个动作,则是需要用户态内核态相互配合完成的操作。而从线程池取的动作就是纯粹用户态的操作。

比如去办卡:

办卡需要身份证复印件,下面有两种方法:

1.张三可以给柜台人员,给柜台人员去复印

这个过程就是涉及到内核态操作,此时,张三也不知道柜台消失之后都做了哪些事情(操作系统的内核,是要给所有的进程提供服务,当你要创建线程的时候,人家内核会帮你做,但是做的过程难免要做一些其他事情),也不知道他啥时候回来,这很显然是不可控的。

2.在大厅的角落,有一个自助复印机,张三可以自行去复印

这个过程就是纯用户态的操作,这样就可以理解为自己复印完后立即返回,整个过程中没有任何拖泥带水,非常可控。

 在java标准库中,也提供了写好了的线程池,直接就可以用了。

 标准库中的线程池

public class ThreadDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}
  • 使用Excutors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池。
  • 返回值类型为ExecutorService。
  • 通过ExcutorService.submit可以注册一个任务到线程池中。

上面的代码是创建线程池的其中一个方式,Executor创建线程池的方式还有几种:

  • newFixedThreadPool:创建固定线程数的线程池。
  • newCachedThreadPool:创建线程数目动态增长的线程池。
  • newSingleThreadExecutor:创建只包含单个线程的线程池。
  • newScheduledThreadPool:设定延迟的时间后执行命令,或者定期执行命令,是进阶版的Timer。

由上面的代码可以看到,线程池对象不是直接new的,而是通过一个专门的方法,返回了一个线程池对象。

像这样通过方法返回一个对象的方法是一种设计模式,即工厂模式。

工厂模式:

很多时候,构造一个对象希望有多种构造方式,,多种构造方式就需要多个版本的构造方法来实现(比如除了方法名不同其他都相同)。但是要多个构造方法需要满足重载(参数类型/个数不同),而使用工厂设计模式就能解决这个问题,使用普通的方法,代替构造方法完成初始化工作,而普通方法就可以使用方法的名字来区别了,也就不再收到重载的规则制约了。

上述这几个创建线程池的工厂方法,本质上都是对一个类(ThreadPoolExecutor)进行的封装

这个类功能丰富,提供了很多参数,标准库上述的几个工厂方法,其实就是给这个类填写了不同的参数用来创建线程池。

这个是从java源码文档中找到的ThreadPoolExecutor的构造方法:

 由于第四个构造方法都包含上面构造方法的参数,下面是对该参数的讲解:

  • int corePoolSize指核心线程数;int maximumPoolSize指最大线程数。这两个参数描述了线程池中线程的数目情况。对于这两个参数我们可以这么理解:把一个线程池比作一个公司,此时公司里面有两类员工:正式员工(有编制的)和实习生(无编制的)。而正式员工的数目就是核心线程数,而最大线程数就是正式员工+实习生。

正式员工允许摸鱼,不会因为摸鱼而被公司开除;而实习生不允许摸鱼,如果这段时间任务多了,就可以找多几个实习生来干活,如果这段时间任务少了,并且少的这个状态持续了一段时间,空闲的实习生就可以裁掉了。这样做既可以满足效率的要求,又可以避免过多的系统开销。

  • long keepAliveTime指允许实习生摸鱼的时间,不是说实习生一摸鱼就立即开除;TimeUnit unit指时间单位:ms,s,min....
  • BlockingQueue<Runnable>  workQueue 指阻塞队列,用来存放线程池中的任务的。这里可以根据需要灵活设置这里的队列是啥,需要优先级,就可以设置PriorityBlockingQueue;如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue;如果不需要优先级且任务数目变动较大。
  • ThreadFactory threadFactory 工厂模式的体现,此处使用ThreadFactory作为工厂类,由这个负责创建线程,使用工厂类创建线程,主要是为了在创建过程中,对线程的属性做出一些设置(如果手动创建线程,就得手动设置这些属性,就比较麻烦,使用工厂方法封装一下)。
  • RejectedExecutionHandler handler指线程池的拒绝策略,一个线程池,能容纳的任务数量有上限,当持续往线程池里添加任务的时候,一旦已经达到上限了,继续再添加,会有不同的效果。

拒绝的策略:

  • ThreadPoolExecutor.AbortPolicys 是直接抛出异常
  • ThreadPoolExecutor.CallerRunsPolicy 新添加的任务,由添加任务的线程负责执行
  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃任务队列中最老的任务
  • ThreadPoolExecutor.DiscardPolicy丢弃当前新加的任务

 而对于使用线程池,设置线程的数目要根据代码的情况来设置。

说明:

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

  1. CPU密集型:代码里主要的逻辑是在进行算术运算/逻辑判断。
  2. IO密集型:代码里主要进行的是IO操作。

 比如一个线程的所有代码都是CPU密集型代码,这个时候,在线程池中创建的数量不应该超过N(设置N就是极限了),设置比N更大的也就无法提高效率了(CPU吃满了),此时更多的线程反而增加调度的开销当一个线程的所有代码都是IO密集型的,这个时候不吃CPU,此时设置的线程数就可以超过N,一个核心可以通过调度的方式来并发执行的。

所以对于线程池线程数目的设置,正确的做法是通过实验的方式,对程序进行性能测试,测试过程中尝试修改线程池中线程的数目。

2.实现线程池

  • 核心操作为submit,将任务加入线程池中。
  • 使用Working类描述一个工作线程,使用Runnable描述一个任务。
  • 使用一个BlockingQueue组织所有任务。
  • 每个worker线程要做的事情:不停的从BlockingQueue中取任务并执行。
  • 指定一下线程池中的最大线程数maxWorkingCount;当当前线程数超过这个最大值时,就不在新增线程了。

代码示例:

class worker extends Thread{
    private LinkedBlockingDeque<Runnable> queue = null;
    public worker(LinkedBlockingDeque<Runnable> queue){
        //因为worker类里面没有线程命名的函数,就调用了父类Thread的构造方法给当前线程命名
        super("worker");
        this.queue = queue;
    }

    //这个工作线程的作用是用来执行阻塞队列中的任务的
    @Override
    public void run() {
        try{
            while(!Thread.interrupted()){
                //把线程池(阻塞队列)中的任务取出来然后执行,为空时会阻塞
                Runnable runnable = queue.take();
                runnable.run();
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class MyThreadPool {
    //当前线程池限制线程的最大数目
    private int maxworkCount = 10;
    //创建一个阻塞链表队列当做线程池
    private LinkedBlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    //把任务提交到线程池中
    public void submit(Runnable command) throws InterruptedException {
        //当任务队列(线程池)的线程数目小于最大线程数时就创建工作线程

        /*
        当队列为满时就不会继续创建工作线程了,进而也不会启动工作线程了
         */
        if(queue.size()<maxworkCount){
            //创建工作线程,把队列传到worker的队列中然后执行
            worker worker1 = new worker(queue);
            worker1.start();
        }
        //往线程池中添加线程,线程池为满时会阻塞直至take
        queue.put(command);
    }

    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool();
        myThreadPool.submit(new Runnable(){
            @Override
            public void run() {
                System.out.println("吃饭");
            }
        });
        Thread.sleep(1000);
        myThreadPool.submit(new Runnable(){
            @Override
            public void run() {
                System.out.println("我吃饭");
            }
        });
        myThreadPool.submit(new Runnable(){
            @Override
            public void run() {
                System.out.println("你吃饭");
            }
        });
        Thread.sleep(1000);
    }
}

说明:线程池的作用在于能够减少线程创建和销毁的成本,而这只是简单的线程池并没有实现线程复用,资源管理等问题。

 另一种方法:

class MyThreadPool{
    //任务队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
    //通过这个方法,把任务添加到队列中
    public void submit(Runnable runnable)throws InterruptedException{
        //相当于是第五种策略了,阻塞等待
        queue.put(runnable);

    }
    public MyThreadPool(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();
        }
    }

}
public class ThreadDemo {
    //创建一个线程池并放入4个线程

    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for(int i =0;i<1000;i++){
//为啥要用id而不直接用i:
//因为变量捕获中,变量的值是不允许再改变的,每循环一次
//i就会变,而每循环一次都创建了一个新的id这两个不一样
            int id = i;
            //提交任务
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务"+id);
                }
            });
        }
    }
}

这个代码核心和上面的一样,用队列存储任务,核心函数submit,执行任务的线程。

说明:但是该代码的输出顺序是不固定的,而第一种的输出顺序是固定的,因为第一种是创建一个工作线程然后start后等待put放入队列中,put后就会有元素有了元素take就会执行,如果把put放到start的上面就会输出不一样的顺序了;而第二种的输出顺序是不固定的,因为它是循环创建多个线程速度很快,等到执行的时候就有很多线程在队列里了,所以这些线程都是并发执行,输出顺序不固定。

 

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

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

相关文章

AI 声音:数字音频、语音识别、TTS 简介与使用示例

在现代 AI 技术的推动下&#xff0c;声音处理领域取得了巨大进展。从语音识别&#xff08;ASR&#xff09;到文本转语音&#xff08;TTS&#xff09;&#xff0c;再到个性化声音克隆&#xff0c;这些技术已经深入到我们的日常生活中&#xff1a;语音助手、自动字幕生成、语音导…

Linux服务器安装mongodb

因为项目需要做评论功能&#xff0c;领导要求使用mongodb&#xff0c;所以趁机多学习一下。 在服务器我们使用docker安装mongodb 1、拉取mongodb镜像 docker pull mongo &#xff08;默认拉取最新的镜像&#xff09; 如果你想指定版本可以这样 docker pull mongo:4.4&#…

Java基础 设计模式——针对实习面试

目录 Java基础 设计模式单例模式工厂模式观察者模式策略模式装饰器模式其他设计模式 Java基础 设计模式 单例模式 单例模式&#xff08;Singleton Pattern&#xff09; 定义&#xff1a;确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问这个实例。适用场景&…

分布式搜索引擎之elasticsearch单机部署与测试

分布式搜索引擎之elasticsearch单机部署与测试 1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器&#xff0c;因此需要让es和kibana容器互联。这里先创建一个网络&#xff1a; docker network create es-net1.2.加载镜像 这里我们采用elasticsearch的7.12.1版本的…

【工具】JS解析XML并且转为json对象

【工具】JS解析XML并且转为json对象 <?xml version1.0 encodingGB2312?> <root><head><transcode>hhhhhhh</transcode></head><body><param>ccccccc</param><param>aaaaaaa</param><param>qqqq<…

CSDN设置成黑色背景(谷歌 Edge)

一.谷歌浏览器 浏览器地址输入&#xff1a;Chrome://flags搜索框输入&#xff1a;enable-force-dark将default 改成 enabled&#xff0c;点击重启浏览器 二.Edge浏览器 浏览器地址输入&#xff1a;edge://flags搜索里面输入Auto Dark Mode for Web Contents将default 改成 e…

【动手学电机驱动】STM32-FOC(8)MCSDK Profiler 电机参数辨识

STM32-FOC&#xff08;1&#xff09;STM32 电机控制的软件开发环境 STM32-FOC&#xff08;2&#xff09;STM32 导入和创建项目 STM32-FOC&#xff08;3&#xff09;STM32 三路互补 PWM 输出 STM32-FOC&#xff08;4&#xff09;IHM03 电机控制套件介绍 STM32-FOC&#xff08;5&…

OGRE 3D----3. OGRE绘制自定义模型

在使用OGRE进行开发时,绘制自定义模型是一个常见的需求。本文将介绍如何使用OGRE的ManualObject类来创建和绘制自定义模型。通过ManualObject,开发者可以直接定义顶点、法线、纹理坐标等,从而灵活地构建各种复杂的几何体。 Ogre::ManualObject 是 Ogre3D 引擎中的一个类,用…

【网络安全 | 漏洞挖掘】绕过SAML认证获得管理员面板访问权限

未经许可,不得转载。 文章目录 什么是SAML认证?SAML是如何工作的?SAML响应结构漏洞结果什么是SAML认证? SAML(安全断言标记语言)用于单点登录(SSO)。它是一种功能,允许用户在多个服务之间切换时无需多次登录。例如,如果你已经登录了facebook.com,就不需要再次输入凭…

AI自动化剪辑工具:可将长视频中精彩部分提取合成短视频

最近&#xff0c;我发现了一款特别适合当下短视频潮流的自动化工具&#xff0c;它能够让我们轻松从长视频中剪辑出精彩片段&#xff0c;并快速生成适合分享的短视频。 这款工具叫 AI Youtube Shorts Generator&#xff0c;是一个开源项目&#xff0c;特别适合那些喜欢制作短视…

Windsurf可以上传图片开发UI了

背景 曾经羡慕Cursor的“画图”开发功能&#xff0c;这不Windsurf安排上了。 Upload Images to Cascade Cascade now supports uploading images on premium models Ask Cascade to build or tweak UI from on image upload New keybindings Keybindings to navigate betwe…

(二)Sping Boot学习——Sping Boot注意事项

1.springboot默认是扫描的类是在启动类的当前包或者下级包。 2.运行报错 ERROR&#xff1a;An incompatible version [1.2.33] of the Apache Tomcat Native library is installed, while Tomcat requires version [1.2.34] 网上试了很多方法&#xff0c;直接重新安装更新版…

Elasticsearch:Retrievers 介绍

检索器&#xff08;retrievers&#xff09;是 Elasticsearch 中搜索 API 中添加的新抽象层。它们提供了在单个 _search API 调用中配置多阶段检索管道的便利。此架构通过消除对复杂搜索查询的多个 Elasticsearch API 调用的需求&#xff0c;简化了应用程序中的搜索逻辑。它还减…

Ubuntu下的Doxygen+VScode实现C/C++接口文档自动生成

Ubuntu下的DoxygenVScode实现C/C接口文档自动生成 1、 Doxygen简介 Doxygen 是一个由 C 编写的、开源的、跨平台的文档生成系统。最初主要用于生成 C 库的 API 文档&#xff0c;但目前又添加了对 C、C#、Java、Python、Fortran、PHP 等语言的支持。其从源代码中提取注释&…

Css—实现3D导航栏

一、背景 最近在其他的网页中看到了一个很有趣的3d效果&#xff0c;这个效果就是使用css3中的3D转换实现的&#xff0c;所以今天的内容就是3D的导航栏效果。那么话不多说&#xff0c;直接开始主要内容的讲解。 二、效果展示 三、思路解析 1、首先我们需要将这个导航使用一个大…

快速理解微服务中Fegin的概念

一.由来 1.在传统的架构里面&#xff0c;我们是通过使用RestTemplate来访问其他的服务&#xff0c;但是这种方式就存在了一个很大的缺陷&#xff0c;也就是被调用方如果发生了服务的迁移(IP和端口发生了变化)&#xff0c;那么调用方也需要同步的在代码里面进行修改&#xff0c;…

【Git】Git 完全指南:从入门到精通

Git 完全指南&#xff1a;从入门到精通 Git 是现代软件开发中最重要的版本控制工具之一&#xff0c;它帮助开发者高效地管理项目&#xff0c;支持分布式协作和版本控制。无论是个人项目还是团队开发&#xff0c;Git 都能提供强大的功能来跟踪、管理代码变更&#xff0c;并保障…

Spring Web MVC(详解中)

文章目录 Spring MVC&#xff08;中&#xff09;RESTFul风格设计RESTFul风格概述RESTFul风格特点RESTFul风格设计规范RESTFul风格好处RESTFul风格实战需求分析RESTFul风格接口设计后台接口实现 基于RESTFul风格练习&#xff08;前后端分离模式&#xff09;案例功能和接口分析功…

什么是GAN?

一、基本概念 生成对抗网络&#xff08;Generative Adversarial Network&#xff0c;GAN&#xff09;是一种由两个神经网络共同组成深度学习模型&#xff1a;生成器&#xff08;Generator&#xff09;和判别器&#xff08;Discriminator&#xff09;。这两个网络通过对抗的方式…

Spring |(八)AOP配置管理

文章目录 &#x1f4da;AOP切点表达式&#x1f407;语法格式&#x1f407;通配符 &#x1f4da;AOP通知类型&#x1f407;环境准备&#x1f407;通知类型的使用 &#x1f4da;AOP通知获取数据&#x1f407;环境准备&#x1f407;获取参数&#x1f407;获取返回值&#x1f407;获…