多线程四大经典案例及java多线程的实现

简介: 多线程四大经典案例及java多线程的实现

本节要点

了解一些线程安全的案例

学习线程安全的设计模型

掌握单例模式,阻塞队列,生产在消费者模型

单例模式

我们知道多线程编程,因为线程的随机调度会出现很多线程安全问题! 而我们的java有些大佬针对一些多线程安全问题的应用场景,设计了一些对应的解决方法和案例,就是解决这些问题的一些套路,被称为设计模式,供我们学习和使用!


单例模式是校招最常考的一个设计模式之一!!!


什么是单例模式呢?


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

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


单例模式的具体实现方法又分为饿汉和懒汉两种!

而这里所说的饿和懒并不是贬义词!

饿汉指的是在创建一个类的时候就将实例创建好!比较急!

懒汉指的是在需要用到实例的时候再去创建实例!比较懒!


饿汉模式

饿汉模式联系实际生活中例子:

就是一个人性子比较急,也许一件事情的期限还有好久,而他却把事情早早干完!


因为我们单例模式只能有一个实例

那如何去保证一个实例呢?

我们会马上想到类中用static修饰的类属性,它只有一份!保证了单例模式的基本条件!


显然生活中这样的人很优秀,但是我们的计算机如果这样却不太好!

因为cpu和内存的空间有限,如果还不需要用到该实例,却创建了实例,那不就增加了内存开销,显然不科学.但事实问题也不大!


class Singleton{
    //饿汉模式, static 创建类时,就创建好了类属性的实例!
    //private 这里的instance实例只有一份!!!
    private static Singleton instance = new Singleton();
    //私有的构造方法!保证该实例不能再创建
    private Singleton(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton getInstance() {
        return instance;
    }
}

我们可以看到这里饿汉模式,当多个线程并发时,并没有出现线程不安全问题,因为这里的设计模式只是针对了读操作!!! 而单例模式的更改操作,需要看懒汉模式!


懒汉模式

联系实际中的例子就是.就是这个人比较拖延,有些事情不得不做的时候,他才会去做完!


//懒汉模式(线程不安全版本)
class Singleton1{
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton1 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton1(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton1 getInstance() {
        if(instance==null){//需要时再创建实例!
            instance = new Singleton1();
        }
        return instance;
    }
}

我们分析一下上述代码,该模式,对singleton进行了修改,而我们知道多线程的修改可能会出现线程不安全问题!

当我们多个线程同时对该变量进行访问时!


我们将该代码的情况分成两种,一种是初始化前要进行读写操作,初始化后只需要进行读操作!

image.png

instance未初始化化前

多个线程同时进入getInstance方法!那就会创建很多次instance实例!

联系之前的变量更改内存和cpu的操作:


显然很多线程进行了无效操作!!!也会触发内存不可见问题!!!

instance初始化后,进行的读操作,就像上面的饿汉模式一样,并没有线程安全问题!

我们下面进行多次优化


//优化1
class Singleton2{
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() {
        synchronized (Singleton.class){ //对读写操作进行加锁!
            if(instance==null){//需要时再创建实例!
                instance = new Singleton2();
            }
            return instance;
        }
    }
}

我们将Singleton类对象加锁后,显然避免了刚刚的一些线程安全问题!但是出现了新的问题!


instance初始化前

在初始化前,我们很好的将读写操作进行了原子封装,并不会造成线程不安全问题!

instance初始化后

然而初始化后的每次读操作却并不好,当我们多个线程进行多操作时,很多线程就会造成线程阻塞,代码的运行效率极具下降!

我们如何保证,线程安全的情况下又保证读操作不会进行加锁,锁竞争呢?


我们可以间代码的两种情况分别处理!


//优化二
class Singleton2{
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() {
        if(instance==null){//如果未初始化就进行加锁操作!
            synchronized (Singleton.class){ //对读写操作进行加锁!
                if(instance==null){//需要时再创建实例!
                    instance = new Singleton2();
                }
            }
        }
        //已经初始化后直接读!!!
        return instance;
    }
}

我们看到这里可能会有疑惑,咋为啥要套两个if啊,把里面的if删除不行吗!!!

我们来看删除后的效果:


//删除里层if
class Singleton2{
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() {
        if(instance==null){//如果未初始化就进行加锁操作!
            synchronized (Singleton.class){ //对读写操作进行加锁!
                    instance = new Singleton2();
            }
        }
        //已经初始化后直接读!!!
        return instance;
    }
}

在删除里层的if后:

我们发现当有多个线程进行了第一个if判断后,进入的线程中有一个线程锁竞争拿到了锁!而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,而这样也就多次创建了instance实例,显然不可!!!


所以这里的两个if都有自己的作用缺一不可!

第一个if:

判断是否要进行加锁初始化

第二个if:

判断该线程实例是否已经创建!


//最终优化版
class Singleton2{
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    //volatile 保证内存可见!!!避免编译器优化!!!
    private static volatile Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2(){
    }
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() {
        if(instance==null){//如果未初始化就进行加锁操作!
            synchronized (Singleton.class){ //对读写操作进行加锁!
             if(instance==null){
                 instance = new Singleton2();
             }
            }
        }
        //已经初始化后直接读!!!
        return instance;
    }
}

而我们又发现了一个问题,我们的编译器是会对代码进行优化操作的!如果很多线程对第一个if进行判断,那cpu老是在内存中拿instance的值,就很慢,编译器就不开心了,它就优化直接将该值存在寄存器中,而此操作是否危险,如果有一个线程将该实例创建!那就会导致线程安全问题! 而volatile关键字保证了instanse内存可见性!!!


总结懒汉模式


双if 外层保证未初始化前加锁,创建实例. 里层if保证实例创建唯一一次

synchronized加锁,保证读写原子性

volatile保证内存可见性,避免编译器优化

阻塞队列

什么是阻塞队列?

顾名思义是队列的一种!

也符合先进先出的特点!

阻塞队列特点:


当队列为空时,读操作阻塞

当队列为满时,写操作阻塞


阻塞队列一般用在多线程中!并且有很多的应用场景!

最典型的一个应用场景就是生产者消费者模型


生产者消费者模型

我们知道生产者和消费者有着供需关系!

而开发中很多场景都会有这样的供需关系!

比如有两个服务器A和B

A是入口服务器直接接受用户的网络请求

B应用服务器对A进行数据提供


在通常情况下如果一个网站的访问量不大,那么A和B服务器都能正常使用!

而我们知道,很多网站当很多用户进行同时访问时就可能挂!

我们知道,A入口服务器和B引用服务器此时耦合度较高!

image.png


当增加就绪队列后,我们就不用担心A和B的耦合!

并且A和B进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!

而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!

也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!


标准库中的就绪队列

我们java中提供了一组就绪队列供我们使用!


BlockingQueue

image.png


BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

put 方法用于阻塞式的入队列,

take 用于阻塞式的出队列.

BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.


//生产着消费者模型
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个阻塞队列
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
        Thread customer = new Thread(() -> {//消费者
            while (true) {
                try {
                    int value = blockingQueue.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);
                    blockingQueue.put(num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者");
        producer.start();
        customer.join();
        producer.join();
    }
}

image.png

阻塞队列实现

虽然java标准库中提供了阻塞队列,但是我们想自己实现一个阻塞队列!


我们就用循环队列实现吧,使用数组!

//循环队列
class MyblockingQueue{
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    public void put(int x){
        //入队操作
        if(size==data.length){
            //队列满
            return;
        }
        data[tail] = x;
        tail++;//入队
        if(tail==data.length){
            //判断是否需要循环回
            tail=0;
        }
        size++; //入队成功加1
    }
    public Integer take(){
        //出队并且获取队头元素
        if(tail==start){
            //队列为空!
            return null;
        }
        int ret = data[start]; //获取队头元素
        start++; //出队
        if(start==data.length){
            //判断是否要循环回来
            start = 0;
        }
       // start = start % data.length;//不建议可读性不搞,效率也低
        size--;//元素个数减一
        return ret;
    }
}

image.png

我们已经创建好了一个循环队列,目前达不到阻塞的效果!

而且当多线程并发时有很多线程不安全问题!

而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!


//阻塞队列
class MyblockingQueue{
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    //锁对象
    Object locker = new Object();
    public void put(int x){
       synchronized (locker){//对该操作加锁
           //入队操作
           if(size==data.length){
               //队列满 阻塞等待!!!直到put操作后notify才会继续执行
               try {
                   locker.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           data[tail] = x;
           tail++;//入队
           if(tail==data.length){
               //判断是否需要循环回
               tail=0;
           }
           size++; //入队成功加1
           //入队成功后通知take 如果take阻塞
           locker.notify();//这个操作线程阻塞并没有副作用!
       }
    }
    public Integer take(){
        //出队并且获取队头元素
        synchronized (locker){
            if(size==0){
                //队列为空!阻塞等待 知道队列有元素put就会继续执行该线程
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            int ret = data[start]; //获取队头元素
            start++; //出队
            if(start==data.length){
                //判断是否要循环回来
                start = 0;
            }
            // start = start % data.length;//不建议可读性不搞,效率也低
            size--;//元素个数减一
            locker.notify();//通知 put 如果put阻塞!
            return ret;
        }
    }
}
//测试代码
public class Test3 {
    public static void main(String[] args) {
            MyblockingQueue queue = new MyblockingQueue();
            Thread customer = new Thread(()->{
                int i = 0;
                while (true){
                    System.out.println("消费了"+queue.take());
                }
            });
                    Thread producer = new Thread(()->{
                        Random random = new Random();
                        while (true){
                            int x = random.nextInt(100);
                            System.out.println("生产了"+x);
                            queue.put(x);
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                    customer.start();
                    producer.start();
    }
}

image.png

可以看到通过wait和notify的配和,我就实现了阻塞队列!!!

image.png



定时器

定时器是什么


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


也就是说定时器有像join和sleep等待功能,不过他们是基于系统内部的定时器,

而我们要学习的是在java给我们提供的定时器包装类,用于到了指定时间就执行代码!

并且定时器在我们日常开发中十分常用!


java给我们提供了专门一个定时器的封装类Timer在java.util包下!


Timer定时器


Timer类下有一个schedule方法,用于安排指定的任务和执行时间!

也就达到了定时的效果,如果时间到了,就会执行task!

image.png


schedule 包含两个参数.

第一个参数指定即将要执行的任务代码,

第二个参数指定多长时间之后执行 (单位为毫秒).

//实例
import java.util.Timer;
import java.util.TimerTask;
public class Demo1 {
    public static void main(String[] args) {
        //在java.util.Timer包下
        Timer timer = new Timer();
        //timer.schedule()方法传入需要执行的任务和定时时间
        //Timer内部有专门的线程负责任务的注册,所以不需要start
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello Timer!");
            }
        },3000);
        //main线程
        System.out.println("hello main!");
    }
}

image.png

我们可以看到我们只需要创建一个Timer对象,然后调用schedule返回,传入你要执行的任务,和定时时间便可完成!


定时器实现

我们居然知道java中定时器的使用,那如何自己实现一个定时器呢!


我们可以通过Timer中的源码,然后进行操作!


Timer内部需要什么东西呢!


我们想想Timer的功能!

可以定时执行任务!(线程)

可以知道任务啥时候执行(时间)

可以将多个任务组织起来对比时间执行


描述任务

也就是schedule方法中传入的TimerTake

创建一个专门表示定时器中的任务

class MyTask{
    //任务具体要干啥
    private Runnable runnable;
    //任务执行时间,时间戳
    private long time;
    ///delay是一个时间间隔
    public MyTask(Runnable runnable,long delay){
            this.runnable = runnable;
            time = System.currentTimeMillis()+delay;
    }
    public void run(){ //描述任务!
        runnable.run();
    }
}

组织任务

组织任务就是将上述的任务组织起来!

我们知道我们的任务需要在多线程的环境下执行,所以就需要有线程安全,阻塞功能的数据结构!并且我们的任务到了时间就需要执行,也就是需要时刻对任务排序!

所以我们采用PriorityBlockingQueue优先级队列!阻塞!

image.png

但是这里我们使用了优先级队列,我们需要指定比较规则,就是让MyTask实现Comparable接口,重写 compareTo方法,指定升序排序,就是小根堆!

image.png


执行时间到了的任务

我们可以创建一个线程,执行时间到了的任务!


//执行时间到了的任务!
    public MyTimer(){
        Thread thread = new Thread(()->{
           while (true){
               try {
                   MyTask task = queue.take();//获取到队首任务
                   //比较时间是否到了
                   //获取当前时间戳
                   long curTime = System.currentTimeMillis();
                   if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
                       //还未到达执行时间
                       queue.put(task); //将任务放回
                   }else{//时间到了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();//启动线程!
    }
//定时器完整代码
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
    //任务具体要干啥
    private Runnable runnable;
    public long getTime() {
        return time;
    }
    //任务执行时间,时间戳
    private long time;
    ///delay是一个时间间隔
    public MyTask(Runnable runnable,long delay){
            this.runnable = runnable;
            time = System.currentTimeMillis()+delay;
    }
    public void run(){ //描述任务!
        runnable.run();
    }
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}
public class MyTimer{
    //定时器内部需要存放多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);//接收一个任务!
        queue.put(task);//将任务组织起来
    }
   //执行时间到了的任务!
    public MyTimer(){
        Thread thread = new Thread(()->{
           while (true){
               try {
                   MyTask task = queue.take();//获取到队首任务
                   //比较时间是否到了
                   //获取当前时间戳
                   long curTime = System.currentTimeMillis();
                   if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
                       //还未到达执行时间
                       queue.put(task); //将任务放回
                   }else{//时间到了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();//启动线程!
    }
}
//测试
public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello Timer");
            }
        }, 3000);
        System.out.println("hello main");
    }

image.png

我们再来检查一下下面代码存在的问题!


//执行时间到了的任务!
    public MyTimer(){
        Thread thread = new Thread(()->{
           while (true){
               try {
                   MyTask task = queue.take();//获取到队首任务
                   //比较时间是否到了
                   //获取当前时间戳
                   long curTime = System.currentTimeMillis();
                   if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
                       //还未到达执行时间
                       queue.put(task); //将任务放回
                   }else{//时间到了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();//启动线程!
    }

我们上述代码还存在一定缺陷就是执行线程到了的代码,我们的while循环一直在处于忙等状态!

就好比生活中:

你9点要去做核酸,然后你过一会就看时间,一会就看时间,感觉就有啥大病一样!

所以我们可以定一个闹钟,到了时间就去,没到时间可以干其他的事情!

此处的线程也是如此!我们这里也可以使用wait阻塞! 然后到了时间就唤醒,就解决了忙等问题!

我们的wait可以传入指定的时间,到了该时间就唤醒!!!


我们再思考另一个问题!


如果又加入了新的任务呢?

我们此时也需要唤醒一下线程,让线程重新拿到队首元素!


//最终定时器代码!!!!
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
    //任务具体要干啥
    private Runnable runnable;
    public long getTime() {
        return time;
    }
    //任务执行时间,时间戳
    private long time;
    ///delay是一个时间间隔
    public MyTask(Runnable runnable,long delay){
            this.runnable = runnable;
            time = System.currentTimeMillis()+delay;
    }
    public void run(){ //描述任务!
        runnable.run();
    }
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}
public class MyTimer{
    //定时器内部需要存放多个任务
    Object locker = new Object();//锁对象
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);//接收一个任务!
        queue.put(task);//将任务组织起来
        //每次拿到新的任务就需要唤醒线程,重新得到新的队首元素!
        synchronized (locker){
            locker.notify();
        }
    }
   //执行时间到了的任务!
    public MyTimer(){
        Thread thread = new Thread(()->{
           while (true){
               try {
                   MyTask task = queue.take();//获取到队首任务
                   //比较时间是否到了
                   //获取当前时间戳
                   long curTime = System.currentTimeMillis();
                   if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
                       //还未到达执行时间
                       queue.put(task); //将任务放回
                       //阻塞到该时间唤醒!
                       synchronized (locker){
                           locker.wait(task.getTime()-curTime);
                       }
                   }else{//时间到了,执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();//启动线程!
    }
}

总结:


描述一个任务 runnable + time

使用优先级队列组织任务PriorityBlockingQueue

实现schedule方法来注册任务到队列

创建扫描线程,获取队首元素,判断是否执行

注意这里的忙等问题

//最后梳理一遍
import java.util.concurrent.PriorityBlockingQueue;
/**
 * Created with IntelliJ IDEA.
 * Description:定时器
 * User: hold on
 * Date: 2022-04-09
 * Time: 16:07
 */
//1.描述任务
class Task implements Comparable<Task>{
    //任务
    private Runnable runnable;
    //执行时间
    private long time;
    public Task(Runnable runnable,long delay){
        this.runnable = runnable;//传入任务
        //获取任务需要执行的时间戳
        time = System.currentTimeMillis() + delay;
    }
    @Override
    public int compareTo(Task o) {//指定比较方法!
        return (int) (this.time-o.time);
    }
    public long getTime() {//传出任务时间
        return time;
    }
    public void run(){
        runnable.run();
    }
}
//组织任务
class MyTimer1{
    private Object locker = new Object();//锁对象
    //用于组织任务
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay){
        Task task = new Task(runnable,delay);
        queue.put(task);//传入队列中
        synchronized (locker){
            locker.notify();//唤醒线程
        }
    }
    public MyTimer1(){
        //扫描线程获取队首元素,判断执行
        Thread thread = new Thread(()->{
           while (true){
               //获取当前时间戳
               long curTimer = System.currentTimeMillis();
               try {
                   Task task = queue.take();//队首元素出队
                   if(curTimer<task.getTime()){
                       //还未到达执行时间,返回队首元素
                       queue.put(task);
                       synchronized (locker){
                           //阻塞等待
                           locker.wait(task.getTime()-curTimer);
                       }
                   }else {
                       //执行任务
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        thread.start();//启动线程
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyTimer1 myTimer1 = new MyTimer1();
        myTimer1.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello Timer1");
            }
        },1000);
        System.out.println("hello main");
    }
}

线程池

我们之前学过常量池!这里的线程池也大同小异!

我们通过创建很多个线程放在一块空间不进行销毁,等到需要的时候就启动线程!避免了创建销毁的时间开销! 提高开发效率!

我们之前不是说一个线程创建并不会划分很多时间吗! 但是我们的多线程编程,有时候需要使用到很多很多线程,如果要进行创建,效率就不高,而线程池或者协程(我们后面会介绍)就避免了创建销毁线程! 但我们需要用到线程时,自己从线程池中给出就好!


我们创建线程的本质还是要通过内核态(就是我们的操作系统)进行创建,然而内核态创建的时间,我们程序员无法掌控,而通过线程池,我们就可以避免了内核态的操作,直接在用户态,进行线程的调用,也就是应用程序层!

image.png

使用线程池大大提高了我们的开发效率!


我们来学习一下java中给我们提供的线程池类,然后自己实现一个线程池!


ThreadPoolExecutor 线程池


这个类在java.util.concurrent 并发编程包下,我们用到的很多关于并发编程的类都在!


可以看到这个线程池有4个构造方法!

image.png

我们了解一下参数最多的那个方法!


public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
创建一个新的 ThreadPoolExecutor与给定的初始参数。
参数
    corePoolSize - 即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut 
    maximumPoolSize - 池中允许的最大线程数 
    keepAliveTime - 当线程数大于内核时,这是多余的空闲线程在终止前等待新任务的最大时间。 
    unit - keepAliveTime参数的时间单位 
    workQueue - 用于在执行任务之前使用的队列。 这个队列将仅保存execute方法提交的Runnable任务。 
    threadFactory - 执行程序创建新线程时使用的工厂 
    handler - 执行被阻止时使用的处理程序,因为达到线程限制和队列容量

我们这里的线程池类比一个公司,便于我们理解该类


int maximumPoolSize,

核心线程数(正式员工)

maximumPoolSize

池中允许的最大线程数(正式员工+临时工)

long keepAliveTime,

多余的空闲线程的允许等待的最大时间(临时工摸鱼时间)

TimeUnit unit,

时间单位

- BlockingQueue<Runnable> workQueue,

任务队列,该类中用一个submit方法,用于将任务注册到线程池,加入到任务队列中!

ThreadFactory threadFactory,

线程工厂,线程是如何创建的

RejectedExecutionHandler handler

拒绝策略

但任务队列满了后怎么做

1.阻塞等待,

2.丢弃久任务

3.忽略新任务

可以看到java给我们提供的这个线程池类让人头大!

但是不必焦虑,我们只需要知道int maximumPoolSize,

核心线程数和 maximumPoolSize 池中允许的最大线程数即可!

面试问题

思考一个问题

我们有一个程序需要多线程并发处理一些任务,使用线程池的话,需要设置多大的线程数?

这里的话,我们无法准确的给出一个数值,我们要通过性能测试的方式找个一个平衡点!


例如我们写一个服务器程序:服务器通过线程池多线程处理机用户请求!如果要确定线程池的线程数的话,就需要通过对该服务器进行性能分析,构造很多很多请求模拟真实环境,根据这里不同的线程数,来观察处理任务的速度和当个线程的cpu占用率!从而找到一个平衡点!

如果cpu暂用率过高,就无法应对一些突发情况,服务器容易挂!


我们java根据上面的ThreadPoolExecutor类进行封装提供了一个简化版本的线程池!Executors供我们使用!

我们通过Executors的使用学习,实现一个线程池!


Executors


java.util.concurrent.Executors


下面都是Executor类中创建线程池的一些静态方法


创建可以扩容的线程池

image.png

我们重点学习创建指定大小得到线程池方法!


//Executors使用案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo3 {
    public static void main(String[] args) {
        //创建一个指定线程个数为10的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello executor!"+ finalI);
                }
            });
        }
    }
}

image.png

我们通过ExecutorService类中的submit可以将多个任务注册到线程池中,然后线程池中的线程将任务并发执行,大大提升了编程效率!可以看到,啪的一下,100个任务给10个线程一下就执行结束了!


实现线程池

我们还是分析一下线程池用什么功能,里面都有些啥!


能够描述任务(直接用runnable)

需要组织任务(使用BlockingQueue)

能够描述工作线程

组织线程

需要实现往线程池里添加任务

//模拟实现线程池
class ThreadPool {
    //描述任务 直接使用Runnable
    //组织任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
    //描述工作线程
    static class Worker extends Thread {//继承Thread类
        BlockingQueue<Runnable> queue = null;
        @Override
        public void run() {
            while (true){
                try {
                    //拿到任务
                    Runnable runnable = queue.take();
                    //执行任务
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //通过构造方法拿到外面的任务队列!
        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }
    }
    //组织多个工作线程
    //将多个工作线程放入到workers中!
    public List<Thread>workers = new LinkedList<>();
    public ThreadPool(int n) {//指定放入线程数量
        for (int i = 0; i < n; i++) {//创建多个工作线程
            Worker worker = new Worker(queue);
            worker.start();//启动工作线程
            workers.add(worker);//放入线程池
        }
    }
    //创建一个方法供我们放入任务
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//测试代码
public class demo5 {
    public static void main(String[] args) {
        //线程池线程数量10
        ThreadPool pool = new ThreadPool(10);
        for (int i = 0; i <100 ; i++) {//100个任务
            int finalI = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello ThreadPool "+ finalI);
                }
            });
        }
    }
}

运行效果

image.png

案例总结

线程安全单例模式

阻塞队列->生产着消费者模型

定时器

MyTask类描述一个任务 Runnable + time

带有优先级的阻塞队列

扫描线程,不停从队首取出元素,检测时间是否到达,并且执行任务,使用wait解决忙等位问题!

实现schedule方法

线程池

描述一个任务Runnable

组织任务,带有优先级的阻塞队列

创建一个工作线程work类,从任务队列获取任务,执行任务

组织工作线程works数据结构存放work

实现一个submit方法将任务放入任务队列中!


目录
相关文章
|
7天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
13天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
37 9
|
16天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
12天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
16天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
31 3
|
14天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
16天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
16天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
44 1
|
20天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
21天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
下一篇
无影云桌面