多线程四大经典案例及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进行了修改,而我们知道多线程的修改可能会出现线程不安全问题!
当我们多个线程同时对该变量进行访问时!

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

  • instance未初始化化前
    多个线程同时进入getInstance方法!那就会创建很多次instance实例!
    联系之前的变量更改内存cpu的操作:
    多线程四大经典案例及java多线程的实现_第1张图片
    显然很多线程进行了无效操作!!!也会触发内存不可见问题!!!
  • 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保证内存可见性,避免编译器优化

阻塞队列

什么是阻塞队列?
顾名思义是队列的一种!
也符合先进先出的特点!
阻塞队列特点:

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

阻塞队列一般用在多线程中!并且有很多的应用场景!
最典型的一个应用场景就是生产者消费者模型

生产者消费者模型

我们知道生产者和消费者有着供需关系!
而开发中很多场景都会有这样的供需关系!
比如有两个服务器AB
A是入口服务器直接接受用户的网络请求
B应用服务器对A进行数据提供

在通常情况下如果一个网站的访问量不大,那么AB服务器都能正常使用!
而我们知道,很多网站当很多用户进行同时访问时就可能挂!
我们知道,A入口服务器和B引用服务器此时耦合度较高!
当一个挂了,那么另一个服务器也会出现问题!
多线程四大经典案例及java多线程的实现_第2张图片
而当我们使用生产者消费者模型就很好的解决了上述高度耦合问题!我们在他们中间加入一个阻塞队列即可!

多线程四大经典案例及java多线程的实现_第3张图片
当增加就绪队列后,我们就不用担心AB的耦合!
并且AB进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!
而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!
也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!

标准库中的就绪队列

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

BlockingQueue

在这里插入图片描述
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();
    }
}

多线程四大经典案例及java多线程的实现_第4张图片

阻塞队列实现

虽然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;
    }
}


多线程四大经典案例及java多线程的实现_第5张图片
我们已经创建好了一个循环队列,目前达不到阻塞的效果!
而且当多线程并发时有很多线程不安全问题!
而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!

//阻塞队列
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();
    }
}

多线程四大经典案例及java多线程的实现_第6张图片
可以看到通过waitnotify的配和,我就实现了阻塞队列!!!

多线程四大经典案例及java多线程的实现_第7张图片

定时器

定时器是什么

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

也就是说定时器有像joinsleep等待功能,不过他们是基于系统内部的定时器,
而我们要学习的是在java给我们提供的定时器包装类,用于到了指定时间就执行代码!
并且定时器在我们日常开发中十分常用!

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

Timer定时器

Timer类下有一个schedule方法,用于安排指定的任务和执行时间!
也就达到了定时的效果,如果时间到了,就会执行task!
多线程四大经典案例及java多线程的实现_第8张图片

  • 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!");
    }
}

多线程四大经典案例及java多线程的实现_第9张图片
我们可以看到我们只需要创建一个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优先级队列!阻塞!
    多线程四大经典案例及java多线程的实现_第10张图片
    但是这里我们使用了优先级队列,我们需要指定比较规则,就是让MyTask实现Comparable接口,重写 compareTo方法,指定升序排序,就是小根堆!
    多线程四大经典案例及java多线程的实现_第11张图片

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

//执行时间到了的任务!
    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");
    }

多线程四大经典案例及java多线程的实现_第12张图片
我们再来检查一下下面代码存在的问题!

 //执行时间到了的任务!
    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");
    }
}

线程池

我们之前学过常量池!这里的线程池也大同小异!
我们通过创建很多个线程放在一块空间不进行销毁,等到需要的时候就启动线程!避免了创建销毁的时间开销! 提高开发效率!
我们之前不是说一个线程创建并不会划分很多时间吗! 但是我们的多线程编程,有时候需要使用到很多很多线程,如果要进行创建,效率就不高,而线程池或者协程(我们后面会介绍)就避免了创建销毁线程! 但我们需要用到线程时,自己从线程池中给出就好!

我们创建线程的本质还是要通过内核态(就是我们的操作系统)进行创建,然而内核态创建的时间,我们程序员无法掌控,而通过线程池,我们就可以避免了内核态的操作,直接在用户态,进行线程的调用,也就是应用程序层!
多线程四大经典案例及java多线程的实现_第13张图片
使用线程池大大提高了我们的开发效率!

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

ThreadPoolExecutor 线程池

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

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

多线程四大经典案例及java多线程的实现_第14张图片我们了解一下参数最多的那个方法!

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 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类中创建线程池的一些静态方法

创建可以扩容的线程池

在这里插入图片描述创建一个指定容量的线程池
在这里插入图片描述创建单线程池
在这里插入图片描述创建一个线程池含有任务队列
在这里插入图片描述
我们重点学习创建指定大小得到线程池方法!

//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);
                }
            });
        }
    }
}

多线程四大经典案例及java多线程的实现_第15张图片
我们通过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);
                }
            });
        }
    }
}

运行效果
多线程四大经典案例及java多线程的实现_第16张图片

案例总结

  • 线程安全单例模式
  • 阻塞队列->生产着消费者模型
  • 定时器
  1. MyTask类描述一个任务 Runnable + time
  2. 带有优先级的阻塞队列
  3. 扫描线程,不停从队首取出元素,检测时间是否到达,并且执行任务,使用wait解决忙等位问题!
  4. 实现schedule方法
  • 线程池
  1. 描述一个任务Runnable
  2. 组织任务,带有优先级的阻塞队列
  3. 创建一个工作线程work类,从任务队列获取任务,执行任务
  4. 组织工作线程works数据结构存放work
  5. 实现一个submit方法将任务放入任务队列中!

你可能感兴趣的:(JAVA,多线程,笔记,java,开发语言)