多线程基础—案例(三)

目录

一、单例模式

1、饿汉模式

2、懒汉模式(单线程版)

3、懒汉模式(多线程版) 

1>synchronized解决原子性问题

2>volatile解决new操作指令重排序问题

二、阻塞式队列

1、是什么?

2、生产者消费者模式

优势

3、标准库中的阻塞队列

4、实现生产者消费者模型

5、阻塞队列模拟实现

三、定时器

1、标准库中的定时器

2、实现定时器 

 定时器 MyTimer

定时任务 MyTimerTask

实现类  

四、线程池 

机制:

优势:

1、Java内置线程池  ThreadPoolEcecutor

阻塞队列 BlockingQueue灵活设置 

拒绝策略 RejectedExecutionHandler

2、通过Executors工厂类创建线程池

3、线程池的线程数目设置 

4、模拟实现一个线程池


一、单例模式

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

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

这种情况下不采用君子协定,而是让编译器进行监督保证这个类在程序中只存在唯一一份实例,这显然是一个明智的决策。

若出现这个类在程序中有多份实例的情况,编译器直接报错(强制要求)。

像final、interface、@Override、throws 本质上都是这种强制要求

1、饿汉模式

类加载的同时创建实例

把构造方法设为私有,则类外的其他代码就无法再实例化Singleno

在类的内部静态实例化Singleno对象instance(Singleno类加载的同时创建实例),每次外界获取实例化对象都是获取到同一个对象instance,这样就保证Singleno类在程序中只存在唯一一份实例。

//希望singleon有唯一实例
class Singleon{
    static private Singleon instance=new Singleon();
    public static Singleon getInstance(){
        return instance;
    }
    private Singleon(){}
}

         这种饿汉模式即便是在多线程的情况下也是线程安全的,因为它在类加载的时候就已经创建了实例,后续只能通过getinstance获取对象,而即便多个线程同时调用getinstance也只是都在进行读取操作,这显然是非常安全的

2、懒汉模式(单线程版)

类加载的时候不创建,第一次使用的时候才创建 

比如需要用记事本查看一个非常大的文件,有2种方式:

        1、先把所有的内容,都加载到内存中,然后再显示内容(加载过程会非常慢)

        2、只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其他内容   

class Singletonlazy{
    private static Singletonlazy instance=null;
    //只会在首次调用getInstance(获取实例对象)的时候才实例化一个对象出来
    public static Singletonlazy getInstance(){
        if(instance==null){
            instance=new Singletonlazy();
        }
        return instance;
    }
}

3、懒汉模式(多线程版) 

上面的懒汉模式是线程不安全的 

        幸运的是线程安全问题只会在首次创建实例时才会出现:多个线程同时调用getinstance方法就可能导致创建出多个实例;

        但实例一旦创建好了,后面在多线程环境调用getinstance就不再有线程安全问题了(不再修改instance了)

1>synchronized解决原子性问题

getinstance方法中的if操作不是原子的,分为 2步:判断对象是否为空,创建对象

为了防止多线程穿插执行,直接把getInstance()方法体加上锁: 

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

 上面我们已经说了,线程安全问题只会出现在首次创建对象的时候,实例一旦创建好了,后面在多线程环境调用getinstance就不再修改instance了,而只是读取返回,这是线程安全的。

但是上述我们改进的代码,后续不管是否已经创建对象,只要一调用getInstance()都需要先加锁

  • 加锁不是说只要加了安全就行的,要加得合理、加得关键。
  • 加锁是一个开销很大的操作
  • 加锁就可能会涉及到锁冲突(锁竞争),一冲突就会引起阻塞等待,性能效率降低

是否有办法既可以让代码线程安全,又保证代码的开销性能效率问题?

既然这样,那先是否已创建对象,若没则加锁进入if-new操作;若已

第一层if对是否加锁进行判断(保证代码开销性能效率);

第二层if是判断是否要创建对象(这里解决if操作的非原子性问题->线程安全问题)

class Singletonlazy{
    private static Singletonlazy instance=null;
    public static Singletonlazy getInstance(){
        if(instance == null) {
            synchronized (Singletonlazy.class) {
                if (instance == null) {
                    instance = new Singletonlazy();
                }
            }
        }
        return instance;
    }
}
2>volatile解决new操作指令重排序问题

new操作,是可能会触发指令重排序的:

new操作可以拆分为3步:

1、申请内存空间

2、在内存空间上构造对象(构造方法)

3、把内存的地址,赋值给instance

        若t1先执行1、3,(申请一个空的内存空间赋值给instance),此时instance就非空了,但是指向的是一个还没初始化的非法对象。

        若此时t2线程开始执行,t2判定instance==null,条件不成立,不加锁;

        阻塞等待一定是两个线程都加锁的时候才会触发,但这里没加锁,t2也就不会等到t1在内存空间上构造对象后再执行

        于是t2线程直接return instance(未初始化的非法的)

        进一步的t2线程的代码就可能会访问instance里面的属性和方法,执行结果就完全不符合预期,即线程不安全

解决这种指令重排序问题就是使用关键字volatile

让volatile修饰Instance,此时就可以使instance在被修改的过程中不会出现指令重排序;

并且保证了内存可见性(多次判断可能会导致编译器优化使用工作内存读取,所以直接强制读写 内存)

class SingletonLazy{
    private static volatile SingletonLazy instance=null;//3、volatile

    public static SingletonLazy getInstance(){
        if(instance==null){
            synchronized (SingletonLazy.class){//1、正确加锁
                if (instance==null)//2、双重判断
                    instance=new SingletonLazy();
            }
        }
        return instance;
    }

    private SingletonLazy(){}
}

这个多线程下的单例模式,看起来很美好了,但是还有问题

1)使用反射打破单例

2)使用序列化/反序列化打破单例

这两种方式就是在故意找茬!滚!

二、阻塞式队列

1、是什么?

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

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

        当队列满时,继续入队就会阻塞,直到有其他线程从队列中取走元素;

        当队列空时,继续出队就会阻塞,直到有其他线程往队列中插入元素.

2、生产者消费者模式

 阻塞队列的一个典型应用场景就是”生产者消费者模型“

小明负责擀饺子皮:不停的在生产 饺子皮(生产者)

小花负责包饺子:不停的消费饺子皮(消费者)

盖帘--阻塞队列(小明放,小花拿)

若小明生产的慢以至盖帘空,则小花就要等待(从空的队列中获取元素要阻塞等待);

若生产的太快以至盖帘满,小明就要等(往满的队列中添加元素要阻塞等待)

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题

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

优势

1、降低资源竞争,提高程序执行效率(减少擀面杖的竞争)

2、阻塞队列使生产者与消费者解耦合

        还是包饺子:擀饺子皮的不关心怎么包饺子;包饺子的不关心怎么擀饺子皮。消费者发生了什么不影响生产者;生产者发生了什么不影响消费者。只要永远生产饺子皮消费饺子皮即可

这点尤其对于分布式系统更加有意义,比如一个简单的分布式系统:

这时A和B直接交互(A把请求发给B,B把响应返回给A),彼此之间的耦合是比较高的:

        1)如果B出现问题,很可能把A也影响到了

        2)如果未来再添加一个C,就需要对A这边的代码做出一定的改动

相比之下,使用阻塞队列引入就能很好解决

(当把阻塞队列封装成单独的服务器程序,这个时候就 把这个队列为“消息队列”)

如果B出问题就不会对A产生影响,(A只是和队列交互,不知道B的存在)

后续如果再新增一个C,A不必进行任何修改,是需要让C从队列中获取数据即可

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

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

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

削峰填谷

生产者消费者结构下,一旦客户端这边发起的请求多了,每个A收到的请求,都会立即发给B,A有多少访问量,B就和A完全一样

但是不同服务器跑的业务不同,虽然访问一样,但是单个访问消耗的硬件资源不一样,可能A承担这些并发量没事,但B承担不住这么多的并发量

这时用阻塞队列用阻塞队列

A收到的了较大的请求量,会把对应的请求写入到队列中;B仍然可以按照之前的 节奏来处理请求

相当于队列帮B承担了压力,B仍按照原来的 节奏处理请求(削峰)

过了峰值,B就继续慢慢的把积压的数据都处理掉(填古)

协调了两者的关系 

有了这样的机制之后,就可以保证在突发情况来临的时候,整个服务器仍然可以正确执行

3、标准库中的阻塞队列

  • BlockingQueue是一个接口,真正实现的类是LinkedBlockingQueue
  • put() 阻塞式入队;take() 阻塞式出队
  • BlockingQueue也有offer、poll、peek等方法,但是这些方法不带有阻塞特性 
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> bqueue=new LinkedBlockingQueue<>();
        bqueue.put("111");
        bqueue.put("222");
        bqueue.put("333");

        String elem=bqueue.take();
        System.out.println(elem);
        elem=bqueue.take();
        System.out.println(elem);
        elem=bqueue.take();
        System.out.println(elem);
        elem=bqueue.take();
        System.out.println(elem);
    }
}

4、实现生产者消费者模型

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class demo10 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue=new LinkedBlockingQueue<>(10);

        Thread customer=new Thread(()->{
            while (true){
                try {
                    int value = queue.take();
                    System.out.println("消费元素:"+value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者");

        customer.start();

        Thread producer=new Thread(()->{
            Random random=new Random();
            while (true){
                    try {
                        int num= random.nextInt(1000);
                        queue.put(num);
                        System.out.println("生产元素:"+num);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
        },"生产者");
        producer.start();
        customer.join();
        producer.join();
    }
}

5、阻塞队列模拟实现

通过循环队列的方式来实现

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){
        if(size== data.length){
            return;
        }
        data[tail]=elem;
        tail++;
        if(tail== data.length){
            tail=0;
        }
        size++;
    }
    public void take(){
        if (size==0){
            return ;
        }
        String ret=data[head];
        head++;
        if(head== data.length){
            head=0;
        }
        size--;
    }
}

上面普通代码中,队列满时可以直接返回 表示队列已满不可再添加;队列空时也可以返回表示不可再删除。但是我们要实现的阻塞队列是:队列满时阻塞等待take()取出后再添加;队列空时阻塞等待put()添加后再进行取出。

要实现这种效果,很明显要用到wait()和notify(),但是这两个方法是需要监视锁synchronized的。

那我们来看下在哪里加锁,发现添加元素和取出元素这两个操作就是不能穿插执行的,ok那就给这两个方法加上锁使他们不穿插执行。

 public synchronized void put(String elem) throws InterruptedException {
            if(size== data.length){
                this.wait();
            }
            data[tail]=elem;
            tail++;
            if(tail== data.length){
                tail=0;
            }
            size++;
            this.notify();
    }

    public synchronized String take() throws InterruptedException{
            if (size==0){
               this.wait();
            }
            String ret=data[head];
            head++;
            if(head== data.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
    }

但是这样还没有万事大吉。

当put()方法因为队列满了而进入wait(),被唤醒时一定就是不满的吗?

有没有可能是被interrupt()方法?若是被interrupt()唤醒则队列仍是满的

interrupt()是可以中断wait状态,当然,使用interrupt会抛出 InterruptedException;但是若像我们上面写的代码这样,整个进程就直接结束了(throws)。

但若用try...catch...直接解决,那么这个运行是会继续的:

public synchronized void put(String elem)  {
            if(size== data.length){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            data[tail]=elem;
            tail++;
            if(tail== data.length){
                tail=0;
            }
            size++;
            this.notify();
    }

但这样被interrupt()打断(队列仍满)而又继续执行会产生什么样的后果?

因为我们这里使用的是基于数组实现循环队列:

 若在队列满size== data.length的情况下再继续执行 data[tail++]=elem,

发现会覆盖元素添加,并且后续这一轮添加也会继续覆盖添加。

所以对于wait是被interrupt唤醒还是被notify唤醒,我们要进行判断,两者的区别就是唤醒后队列是否满。若仍满,则是被interrupt唤醒;若不满,则是被notify唤醒。

但若是再循环嵌套一层 if(size== data.length){this.wait();}发现这不行啊,那这一层又要判断wait是被啥唤醒的.....无止尽了还...既然要每次被唤醒都判断一下,何不要用size== data.length作为while循环的条件呢?!直到判断到队列不为空才进行下面的添加元素。

take()中的wait()判断条件同理

而Java官方文档就给出的也是这样:使用wait时,往往都是使用while作为条件判断的方式,目的就是为了让wait唤醒之后还能再确认一次条件是否满足

public synchronized void put(String elem) throws InterruptedException {
            while(size== data.length){
                    this.wait();
            }
            data[tail]=elem;
            tail++;
            if(tail== data.length){
                tail=0;
            }
            size++;
            this.notify();
    }

    public synchronized String take() throws InterruptedException{
            while (size==0){
               this.wait();
            }
            String ret=data[head];
            head++;
            if(head== data.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
    }

还有我们定义的那几个成员变量,在操作中有的要进行读有的要修改,为了避免内存可见性问题给他们加上volatile;而数组只是在对里面的内容进行修改而不影响data本身,所以不必理会。

class MyBlockingQueue{
    private String[] data=new String[1000];
    private volatile int head=0;
    private volatile int tail=0;
    private volatile int size=0;//有效元素个数

    public synchronized void put(String elem) throws InterruptedException {
            while(size== data.length){
                    this.wait();
            }
            data[tail]=elem;
            tail++;
            if(tail== data.length){
                tail=0;
            }
            size++;
            this.notify();
    }

    public synchronized String take() throws InterruptedException{
            while (size==0){
               this.wait();
            }
            String ret=data[head];
            head++;
            if(head== data.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
    }
}

三、定时器

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

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

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

1、标准库中的定时器

标准库中的提供了一个Timer类,Timer类的核心方法为schedule

schedule包含2个参数:第一个指定即将要执行的代码任务,第二个指定时间

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

加点东西运行一下: 

import java.util.Timer;
import java.util.TimerTask;

public class Test {
    public static void main(String[] args) {
        Timer timer=new Timer();
        //timer.schedule(定时器任务,延迟时间);
        //public abstract class TimerTask implements Runnable
        timer.schedule(new TimerTask() {//此处使用匿名内部类的写法,继承了TimerTask并且创建出了一个实例
            @Override
            public void run() {//目的也是为了重写run(),描述任务的内容
                System.out.println("正在执行定时器的任务");
            }
        },2000);
        System.out.println("程序启动!");

    }
}

import java.util.Timer;
import java.util.TimerTask;

public class Test1 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            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("程序启动!");
    }
}

2、实现定时器 

不光要学习如何使用定时器,还要学习如何实现一个简单的定时器

1>MyTimer中需要一个扫描线程,用于判定是否到达执行任务时间

2>MyTimer中还需要一个数据结构(优先级队列),把所有任务都保存起来

        给Timer添加的任务,都是带有一定“延时时间”,一定是延时时间最小的的最先执行

        对于优先级队列来说,里面的元素必须是可比较的

3>Timer中的schedule方法用于传参

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

但此时这个优先级队列中的任务还是不可比较的,在MytinerTask类实现Comparable接口并重写比较方法才可比较

 

  •  定时器 MyTimer
class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    Object locker=new Object();

    public void schedule(Runnable runnable,long delay){
        synchronized (locker){
            queue.offer(new MyTimerTask(runnable,delay));
            locker.notify();
        }

    }
    //扫描线程
    public MyTimer(){
        //创建一个扫描线程
        Thread thread=new Thread(()->{
            //扫描线程需要一直扫描队首元素,看着是否到达时间
            while (true){
                try {
                    synchronized (locker){
                        //不要使用if,要使用while(在wait被唤醒时再次判断队列是否为空)
                        while (queue.isEmpty()){//队列为空-->阻塞等待
                            locker.wait();//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();
                }
            }
        });
        thread.start();
    }
    //wait需要锁起来--正好不同线程都要操作同一个队列,schedle()、MyTimer() 有线程安全问题--锁起来怕中间有穿插
}
  • 定时任务 MyTimerTask
//定时任务
class MyTimerTask implements Comparable<MyTimerTask>{   
    private Runnable runnable;  
    private long time;
  

    public MyTimerTask(Runnable runnable, long delaytime) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis()+delaytime;//传入延迟时间(相对时间),保存要执行时的绝对时间
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time-o.time);
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}
  • 实现类  
public class demo1 {
    public static void main(String[] args) {
        MyTimer timer=new MyTimer();
        timer.schedule(new TimerTask() {
            @Override
            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("程序启动!");
    }
}

四、线程池 

虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程时还是会比较低效

线程池就是为了解决这个问题。如果某个线程不再使用了,并不是真正把线程释放,而是放到一个”池子“中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了

线程池是一种用于管理和复用线程的机制:
  1. 在应用程序启动时,线程池会预先创建一定数量的线程,并将它们保存在池中待命;
  2. 当有任务需要执行时,线程池会从池中取出一个空闲线程来处理该任务;
  3. 任务执行完毕之后,该线程并不会被销毁,而是返回到线程池中,再次变为空闲状态,等待任务;

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

     (一个程序是在系统内核中完成的--内核态;若不是--用户态)

        创建线程,就需要调用系统api,进入到内核中,按照内核态的方式来完成一系列动作,内核中有太多工作,频繁创建线程显然效率大大降低

优势:
  1. 线程和任务分离,提高线程重用性,降低资源消耗
  2. 复用已存在的线程,避免了频繁创建和销毁线程所带来的性能开销,提高响应速度
  3. 控制线程并发数量,降低服务器压力,提高系统的性能和资源利用率,便于对线程进行统一的管理

1、ThreadPoolEcecutor

理解ThreadPoolEcecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,/*核心线程数量*/
                              int maximumPoolSize,/*最大线程数量*/
                              long keepAliveTime,/*最大空闲时间*/
                              TimeUnit unit,/*时间单位*/
                              BlockingQueue<Runnable> workQueue,/*任务队列*/
                              ThreadFactory threadFactory,/*线程工厂*/
                              RejectedExecutionHandler handler){}/*拒绝策略*/

把创建一个线程想象成开个公司,每个员工相当于一个线程

  • corePoolSize: 正式员工的数量(一旦录用,永不辞退)
  • maximumPoolSize:正式员工+临时员工的数目(临时工:一段时间不干活就被辞退)
  • keepAliveTime:临时工允许的空闲时间
  • unit:keepaliveTime的时间单位(分、秒....)
  • workQueue:传递任务的阻塞队列
  • threadFactory:创建线程的工厂,参与具体的创建线程工作
  • RejectedExecutionHandler:拒绝策略,若任务量超出公司的负荷接下来怎么处理 
  • 任务队列 BlockingQueue workQueue灵活设置 

            需要优先级 -> PriorityBlockingQueue
            不需要优先级 & 执行任务相对恒定 -> ArrayBlockingQueue
            不需要优先级 & 任务数目变动较大 -> LinkedBlockiingQueue

  • 拒绝策略 RejectedExecutionHandler

        当 提交任务数 > 阻塞队列长度 + 最大线程数 时,触发拒绝策略 

   当提交任务数 > 核心线程数量时,会优先放到阻塞队列中,当阻塞队列也饱和了会扩充线程池中的线程数量,直到达到最大线程数量,再添加多余的任务则会触发线程池的拒绝策略

AbortPolicy直接抛出异常,中止任务
CallerRunsPolicy调用者(线程)负责执行
DiscardOldestPolicy丢弃任务队列中最老的任务
DiscardPolicy丢弃当前新加的任务
代码实例
ExecutorServicepool=newThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,newSynchronousQueue<Runnable>(), Executors.defaultThreadFactory(),newThreadPoolExecutor.AbortPolicy());  
for(inti=0;i<3;i++) {
    pool.submit(newRunnable() {
        @Override
        voidrun() {
            System.out.println("hello");
        }
    });
}
线程池的工作流程: 

 

2、通过Executors工厂类创建线程池

Executors本质上是ThreadPoolExecuto类的封装 

Executors创建线程池的几种方式

  • newFixedThreadPool:固定数量
  • newCachedThreadPool:线程数目动态增长
  • newSingleThreadExecutor:只包含单个线程
  • newScheduleThreadPool:设定延迟时间后执行命令,或者定期执行命令(进阶版Timer)
  • Executors本质上是ThreadPoolExecutor类的封装,ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定

Executors是一个工厂类,能够创建出几种不同风格的线程池

ExecutorService表示一个线程池实例

ExecutorService.submit() 可以向线程池提交任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class demo8 {
    public static void main(String[] args) {
        ExecutorService pool= Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello!");
            }
        });
    }
}

Executors创建线程的几种方式体现了工厂模式(填补构造方法的不足)

比如,若希望有多种方式构造一个类,但是构造方法的方法名又必须为类名,这就只能通过重载区分,但是重载又要求参数类型/个数不同,那我们可能会遇到下面的情况:

class Point{
    public Point(double x,double y){}//使用笛卡尔坐标系构造
    public Point(double r,double a){}//使用极坐标构造
}

很显然这上面两种构造方法不满足重载,这就受到构造方法的限制了 

工厂模式就用普通方法替代构造方法,通过方法名来区分,不再受到重载的规则制约

class PointFactory{
    public static Point makePointByXY(double x,double y){}
    public static Point makePointByRA(double r,double a){}
}

3、线程池的线程数目设置  

 设cpu核心数(逻辑核心数)是N

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

  • cpu密集型:代码里主要的逻辑是在进行 算术逻辑/逻辑判断
  • O密集型:代码里主要进行的是IO操作

假设为cpu密集型,则线程数目应小于N,若再多反而增加调度的开销,效率降低(cpu已吃满);

若为IO密集型,则可大于N。

代码不同,线程池的线程数目设置就不同。无法知道一个代码具体内容是cpu密集型,多少内容是io密集。

正确做法:使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程池的数目,看哪种情况做符合你的要求

4、模拟实现一个线程池

由上面的ThreadPoolExecutor构造方法可知,线程池主要由2部分组成:任务队列(存储等待执行的任务),工作者线程(从任务队列中取出任务并执行的线程)

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

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(()->{
                Runnable runnable= null;
                try {
                    runnable = queue.take();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                runnable.run();
            });
            t.start();
        }
    }
}
public class demo9 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool=new MyThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            int id=i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务:"+id);//现在捕获的不是i了,而是id(变量捕获捕获final)
                }
            });
        }
    }
}

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值