0


手把手教你如何拿捏多线程编程四大案例

文章目录

案例一:线程安全的单例模式

线程安全的单例模式分为:饿汉模式 和 懒汉 模式

  1. 饿汉的单例模式,是比较着急的去进行创建实例的
  2. 懒汉的单例模式,是不太着急的去创建实例,知识在用的时候,才去正在创建

1.饿汉模式

在这里插入图片描述

package Thread;

//通过 Singleton 这个类来实现单例模式,保证Singleton 这个类只有一个实例
    //饿汉模式
class Singleton {
    //1.使用 static 立即创建一个实例,并且立即进行实例化
    // 这个 instance 对应的实例,就是该类的唯一实例
    private static Singleton instance = new Singleton();
    //2.为了防止程序员在其他地方不小心的 new 这个 Singleton,就可以把构造方法设为 private
    private Singleton() {}
    //3.提供一个方法,让外面能够拿到唯一实例
    public static Singleton getInstance() {
        return instance;
    }

}
public class TestDmeo1 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
    }
}

2.懒汉模式

在这里插入图片描述

package Thread;

//实现单例模式 - 懒汉模式
class Singleton2 {
    //1. 就不是立即就初始化实例
    private static Singleton2 instance = null;
    //2. 把构造方法设为 private
    public Singleton2() {

    }
    //3. 提供一个方法来获取上述单列的实例
    //  只有当我们真正需要用到这个实例的时候,才会真正的去创建这个实例
    public static Singleton2 getInstance() {
        //如何保证懒汉模式的线程安全,加锁!!
        //如果这个条件成立,说明当前的单例未初始化过的,存在线程安全风险,就需要加锁
        if(instance == null) {
            synchronized (Singleton2.class) {//这里需要指定一个锁对象(这里的类对象)
                if(instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        Singleton2 instance2 = Singleton2.getInstance();
    }
}

3.实现一个线程安全的单例模式

在这里插入图片描述
这是一道非常经典的面试题,我们在学习这个模式的时候一定要抓住以下三点:

  1. 正确的位置加锁
  2. 双重 if 判定
  3. volatile

以下是具体代码:

package Thread;

//实现单例模式 - 懒汉模式
class Singleton2 {
    //1. 就不是立即就初始化实例
    private static Singleton2 instance = null;
    //2. 把构造方法设为 private
    public Singleton2() {

    }
    //3. 提供一个方法来获取上述单列的实例
    //  只有当我们真正需要用到这个实例的时候,才会真正的去创建这个实例
    public static Singleton2 getInstance() {
        //如何保证懒汉模式的线程安全,加锁!!
        //如果这个条件成立,说明当前的单例未初始化过的,存在线程安全风险,就需要加锁
        if(instance == null) {
            synchronized (Singleton2.class) {//这里需要指定一个锁对象(这里的类对象)
                if(instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        Singleton2 instance2 = Singleton2.getInstance();
    }
}

案例二:阻塞队列

阻塞队列:符合先进先出规则的队列,相比于普通队列,阻塞队列又有一些其他方面的功能

特征:

  1. 线程安全
  2. 产生阻塞效果:如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止 如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止

基于上诉特征就可以实现"生产消费者模型",而阻塞就可以作为生产着消费者模型中的交易场所

1.生产消费者模型

生产消费者模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中

假设:有两个服务器AB,A作为入口服务器直接接收用户的网络请求,B作为应用服务器来给A提供一些数据
在这里插入图片描述
如果不使用生产者消费模型,此时A和B的耦合性是比较强的,在开发A代码的时候就得充分了解B提供的一些接口,开发B代码的时候也得充分了解到A是怎么调用的,一旦想把B换成C,A的代码就需要较大的改动,而且如果B挂了,也就可以直接导致A也顺带挂了

2.生产者消费者模型优点

优点1:能够让多个服务器程序之间更充分的解耦合

在这里插入图片描述
对于请求:A是生产者,B是消费者
对于响应:A是消费者,B是生产者
阻塞队列作为交易场所
此时,A只需要关注如何和阻塞队列交互,不需要认识B
B也只需要关注如何和阻塞队列交互,也不需要认识A(队列是不变)
如果B挂了,对于A来说没有影响,如果把B换成C,A也完全感知不到

优点2:能够对于请求进行"削峰填谷"

不使用生产者消费者模型:
在这里插入图片描述
使用生产者消费者模型:
在这里插入图片描述

3.阻塞队列的用法(代码实现)

在这里插入图片描述

4.生产者消费者模型(代码实现)

在这里插入图片描述

package Thread;
class MyBlockingQueue {
    private int[] date = new int[1000];
    //有效数据个数
    private int size = 0;
    //队首下标
    private int head = 0;
    //队尾下标
    private int tail = 0;

    //专门写一个锁对象
    private Object locker = new Object();
    //入队列
    public void put(int value) throws InterruptedException {
        //线程安全问题直接加锁
        synchronized (locker) {//锁对象,如果没有锁对象 this
            if(size == date.length) {
                //队列满了,暂时先直接返回
                //return;
                locker.wait();
            }
            //把新的元素放到 tail 位置上
            date[tail] = value;
            tail++;
            //处理 tail 到达数组末尾的情况
            if(tail >= date.length) {
                tail = 0;
            }
            size++;//插入完成之后要修改元素个数
            //如果入队列为空,则嘟列非空,于是唤醒take中的阻塞等待
            locker.notify();
        }

    }

    //出队列
    public Integer take() throws InterruptedException {
        synchronized (locker) {
            if(size == 0) {
                //如果队列为空,就返回一个非法值
                //return null;
                locker.wait();
            }
            //取出 head 位置的元素
            int ret = date[head];
            head++;
            if(head >= date.length) {
                head = 0;
            }
            size--;
            //take 成功之后,就唤醒put 中的等待
            locker.notify();
            return ret;
        }
    }
}
public class TestDemo4 {
    public static void main(String[] args) {
        //简单验证
        MyBlockingQueue queue = new MyBlockingQueue();
       // queue.put(1);
        int ret = 0;
        // ret = queue.take();
    }
}

案列三:定时器

定时器,像是一个闹钟,进行定时,在一定时间之后,被唤醒并执行某个之前设定好的任务

java.util.Timer****核心方法就一个schedule(安排),参数有两个:任务是什么,多长时间之后执行

在这里插入图片描述

1.Timer 内部都需要什么

  1. 描述任务:创建一个专门的类来表示一个定时器中的任务(TimerTask)

在这里插入图片描述

  1. 组织任务(使用一定的数据结构把一些任务给放到一起)

在这里插入图片描述
注意:**PriorityBlockingQueue<>()**既带优先级又带有阻塞队列
在这里插入图片描述

  1. 执行时间到了的任务:需要执行时间最靠前的任务,就需要有一个线程,不停地去检查当前优先队列的队首元素,看看当前最靠前的这个任务是不是时间到了

在这里插入图片描述

2.上述代码存在的缺陷

如果在main函数当中运行上述代码你会发现

在这里插入图片描述

所以上述代码中存在两个非常的缺陷

第一个缺陷解决方法,实现一个Comparable接口去比较时间的大小:

在这里插入图片描述

当我们实现 Comparable 接口之后 执行代码,就能正常运行
在这里插入图片描述

2.第二个缺陷:

在这里插入图片描述

解决办法:使用 wait 机制
在这里插入图片描述

在这里插入图片描述

3.完整代码实现

package Thread;

import java.util.Timer;
import java.util.concurrent.PriorityBlockingQueue;

//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask> {
    //任务具体要干啥
    private Runnable runnable;
    //任务具体什么时候干,保证任务要执行的毫秒级时间戳
    private long delay;

    //after 是一个时间间隔,不是绝对的时间戳
    public MyTask(Runnable runnable,long after) {
        this.runnable = runnable;
        this.delay = System.currentTimeMillis()+after;
    }
    public void run(){
        runnable.run();;
    }
    public long getTime() {
        return delay;
    }
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.delay - o.delay);
    }
}
class MyTimer {
    //定时器内部要能够存放多个任务
    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();
        }
    }

    private Object locker = new Object();//创建一个锁对象

    public MyTimer() {
        Thread t = new Thread(()-> {
            while (true) {
                try {
                    //先取出队首元素
                    MyTask task = queue.take();
                    //再比较一下看看当前这个任务时间到了没
                    long cutTime = System.currentTimeMillis();
                    if(cutTime < task.getTime()) {
                        //时间美刀,把这个任务再塞回到队列中
                        queue.put(task);
                        //指定一个等待时间
                        synchronized (locker) {
                            locker.wait(task.getTime() - cutTime);
                        }
                    }else {
                        //时间到了,执行这个任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
public class TestDemo25 {
    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("main");
    }
}

案例四:线程池

把线程提前创建好,放到池子里,后面需要用线程,直接从池子里取,就不必从系统这边申请

1.线程池的优势

总体来说,线程池有如下的优势:

  1. 降低资源消耗 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度 当任务到达时,任务可以不需要等待线程的创建就能立即执行
  3. 提高线程的可管理性 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.线程池的构造方法

线程池的真正实现类是 ThreadPoolExecutor .其构造方法有4种,在这里我们主要强调一种:
在这里插入图片描述

把一个线程池,想象成是一个"公司",公司里有很多的员工在干活,把员工分成两类:1.正式员工 2.临时工
接下来我们在来看这个构造方法里面的参数代表的含义:

  1. int corePoolSize 核心线程数(正式员工的数量)
  2. int maximumPoolSize 最大线程数(正式员工+临时工)
  3. long keepAliveTime 允许临时工摸鱼的时间
  4. TimeUnit 时间的单位(s,ms,us…)
  5. BlockingQueue < Runnable > workQueue 任务队列,线程池会提供一个 submit 方法 让程序猿把任务注册到线程池中,加到这个任务队列中
  6. ThreadFactory threadFactory 线程工厂,线程是怎么创建出来的
  7. RejectedExecutionHandler handler 拒绝策略

3.线程池代码的实现

标准库中提供了一个简化版的线程池 Executors 本质是针对 ThreadPoolExecutor 进行了封装,提供了默认参数,看一下 Executors 是如何使用的,我们仿照这个实现一个线程池

创建个固定线程数目的线程池,遍历100次"hello threadpool"

在这里插入图片描述
输出结果:
在这里插入图片描述
创建个简单的线程池实例:

首先我们需要知道线程池里有什么

  1. 先能够描述任务(直接使用 Runnable)
  2. 需要组织任务(直接使用BlockingQueue)
  3. 能够描述工作线程
  4. 还需要组织这些线程(利用数据结构)
  5. 需要实现,往线程池里添加任务

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package Thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool {
    //1. 描述一个任务,直接使用 Runnable 不需要额外创建类了
    //2. 使用一个数据结构来自组织若干个任务
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    //3.描述一个线程,工作线程的功能就是从任务队列中取任务并执行
    static class Worker extends Thread {
        //当前线程池中有若干个 Worker 线程 这线线程内部 都持有了上述的任务队列
        private BlockingDeque<Runnable> queue = null;
        public Worker(BlockingDeque<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            //需要拿到上面的队列
            while (true) {
                try {
                    //循环的去获取任务队列中的任务
                    //这里如过队列为空,就直接阻塞,如过队列非空,就获取到里面的内容
                    Runnable runnable = queue.take();
                    //获取之后,就执行任务
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //4.创建一个数据结构来组织若干个线程
    private List<Thread> workers = new ArrayList<>();
    public MyThreadPool(int n) {
        //在构造方法中,创建出若干个线程,放到上述的数组中
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    //5.创建一个方法,能够允许程序员来放任务到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}
public class TestDemo26 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

本文转载自: https://blog.csdn.net/Biteht/article/details/123853956
版权归原作者 青花瓷~ 所有, 如有侵权,请联系我们删除。

“手把手教你如何拿捏多线程编程四大案例”的评论:

还没有评论