【Javaweb】多线程案例

目录

一、单例模式

1,什么是单例模式

2,单例模式分类

二,生产者消费者模型

1,阻塞队列是什么

2,标准库中的阻塞队列

3,生产者消费者模型

三、定时器

1,定时器是什么

2,标准库中的定时器

3,实现定时器

四、线程池

1,线程池是什么

2,标准库中的线程池

3,实现线程池


一、单例模式

1,什么是单例模式

设计模式好比象棋中的 "棋谱". 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏

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

2,单例模式分类

饿汉模式

类加载的同时, 创建实例.

//先创建一个表示单例的类
    //我们要求Singletion这个类只能有一个实例
    //饿汉模式,“饿”是指类被加载,实例就会被创建
    static class Singletion{
        //把构造方法变为私有,此时该类外部就无法new这个类的实例
        private Singletion(){}

        //创建一个static的成员,表示该类的唯一实例
        private static Singletion instance = new Singletion();

        public static Singletion getInstance() {
            return instance;
        }
    }

    public static void main(String[] args) {
        Singletion s1 = Singletion.getInstance();
        Singletion s2 = Singletion.getInstance();
        System.out.println(s1 == s2);
    }

懒汉模式

单线程

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

但以上代码存在线程不安全的问题

对于懒汉模式,多线程调用getInstance,getInstance做了四件事

        读取instance的内容

        判断instance是否为空

        如果instance为null,就new实例

        返回实例地址

 当一个类被new两次,就不是单例模式了,如果线程更多,new的次数可能更多,不符合单例模式

多线程

static class Singletion{
        private Singletion() { }
        private static Singletion instance = null;

        public static Singletion getInstance(){
            if (instance == null){
                instance = new Singletion();
            }
            return instance;
        }

    }

保证线程安全,我们选择加锁

static class Singletion{
        private Singletion() { }
        private static Singletion instance = null;

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

 

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作

当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例

 static class Singletion{
        private Singletion() { }
        private static Singletion instance = null;

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

 涉及多个读操作,可能会被编译器优化,为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile。

static class Singletion{
        private Singletion() { }
        private volatile static Singletion instance = null;

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

    }

为了保证线程安全,涉及三个要点

        1,加锁保证线程安全

        2,双重if保证效率

        3,volatile避免了内存可见性的问题

二,生产者消费者模型

1,阻塞队列是什么

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则. 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

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

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

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型

2,标准库中的阻塞队列

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

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

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

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

BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();

3,生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

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

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

2) 阻塞队列也能使生产者和消费者之间 解耦

        比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺 子皮的人就是 "生产者", 包饺子的人就是 "消费者". 擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人 也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超 市买的).

static class BlockingQueue{
        //基于链表
        //基于数组
        private int[] array = new int[100];
        private volatile int head = 0;
        private volatile int tail = 0;
        //head和tail构成的是一个前闭后开的区间
        //当两者重合的时候,可能表示队列空,也可能是表示队列满
        //为了区分空还是满,需要额外引入一个size来表示

        private volatile int size = 0;
        //入队列 出队列 取队首元素
        public void put(int value) throws InterruptedException {
            synchronized (this){
                while (size == array.length){
                    wait();
                }

                //把value放在队尾即可
                array[tail] = value;
                tail++;
                if (tail == array.length){
                    tail = 0;
                }
                size++;
                notify();
            }
        }

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

    public static void main(String[] args) {
        BlockingQueue blockingQueue = new BlockingQueue();
        //第一次,让消费者消费的快一些,生产者生产的慢一些
        //就会看到消费者阻塞等待,每次有新的生产者生产元素的时候,消费者才能消费
        //第二次,让消费者消费的慢一些,生产者生产的快一些
        //就会看到生产者往队列中插入元素,插入元素满了之后就会阻塞等待
        //随后消费者每次消费一个元素,生产才能产生新的元素
        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++){
                    try {
                        blockingQueue.put(i);
                        System.out.println("生产元素:" + i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        Thread consumer = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        int ret = blockingQueue.take();
                        System.out.println("消费元素:" + ret);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        consumer.start();
    }

 此处两个wait触法的条件,一个是队列为空,一个是队列为满

实现阻塞队列的特点:

        1,队列为空,继续出队列就会阻塞,一直阻塞到其他线程入队列为止

        2,队列为满,继续入队列也会阻塞,一直阻塞到其他线程出队列为止

public static void main(String[] args) {
        BlockingQueue blockingQueue = new BlockingQueue();
        //第一次,让消费者消费的快一些,生产者生产的慢一些
        //就会看到消费者阻塞等待,每次有新的生产者生产元素的时候,消费者才能消费
        //第二次,让消费者消费的慢一些,生产者生产的快一些
        //就会看到生产者往队列中插入元素,插入元素满了之后就会阻塞等待
        //随后消费者每次消费一个元素,生产才能产生新的元素
        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++){
                    try {
                        blockingQueue.put(i);
                        System.out.println("生产元素:" + i);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        Thread consumer = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        int ret = blockingQueue.take();
                        System.out.println("消费元素:" + ret);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        consumer.start();
    }

or (int i = 0; i < 10000; i++){
                    try {
                        blockingQueue.put(i);
                        System.out.println("生产元素:" + i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        Thread consumer = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        int ret = blockingQueue.take();
                        System.out.println("消费元素:" + ret);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        consumer.start();
    }

三、定时器

1,定时器是什么

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

2,标准库中的定时器

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

        schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).

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

3,实现定时器

定时器的构成:

        一个带优先级的阻塞队

        队列中的每个元素是一个 Task 对象.

        Task 中带有一个时间属性, 队首元素就是即将

        同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行

import java.util.concurrent.PriorityBlockingQueue;

public class Thread4 {

    //优先级队列的元素必须是可比较的
    //比较规则有两种:
    //1,让Task实现Comparable接口
    //2,让优先级队列构造的时候,传入一个对象比较器
    static class Task implements Comparable<Task>{
        //Runnable中有一个run方法,借助run方法来执行具体的任务是啥
        private Runnable command;
        //time表示啥时候来执行command,是一个绝对时间
        private long time;
        //构造方法的参数
        public Task(Runnable command,long after){
            this.command = command;
            this.time = System.currentTimeMillis() + after;
        }

        //具体任务的逻辑
        public void run(){
            command.run();
        }

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

    static class Worker extends Thread{
        private PriorityBlockingQueue<Task> queue = null;
        private Object mailBox = null;

        public Worker(PriorityBlockingQueue<Task> queue, Object mailBox){
            this.queue = queue;
            this.mailBox = mailBox;
        }

        @Override
        public void run() {
            while (true){
                try{
                    //1,取出队首元素
                    Task task = queue.take();
                    //2,检查当前任务时间是否到了
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime){
                        //时间还没到,把任务塞回队列中
                        queue.put(task);
                        synchronized (mailBox){
                            mailBox.wait(task.time - curTime);
                        }
                    }else {
                        task.run();
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                    break;
                }
            }
        }
    }

    static class Timer{
        //为了避免忙等,需要使用wait方法
        //使用一个单独的对象来辅助wait
        //使用this也行
        private Object mailBox = new Object();
        //定时器的基本组成
        //1,用一个类来描述队伍
        //2.用一个阻塞优先级队列来组织若干任务,让队首元素就是时间最早的任务,如果队首元素时间未到,其他元素也不能执行
        //3.用一个线程来循环扫描当前阻塞队列的队首元素,如果时间到了,就让执行指定任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();

        public Timer(){
            //创建线程
            Worker worker = new Worker(queue,mailBox);
            worker.start();

        }
        //4.还需要提供一个方法,让调用者把任务给“安排“进来
        public void schedule(Runnable command,long after){
            Task task = new Task(command,after);
            queue.put(task);
            synchronized (mailBox){
                mailBox.notify();
            }
        }
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("he he");
                timer.schedule(this,2000);
            }
        },2000);
    }


}

1,有一个类来描述一个任务

2,有一个阻塞优先级队列来组织这些任务

 3,有一个扫描线程,定时扫描队首元素

 4,有一个接口,让调用者安排任务给定时器

 5,解决忙等问题,加上wait和notify

 初始情况下,队列为空,阻塞是在第一个位置take阻塞,调用schedlule会唤醒take位置的阻塞,取到task任务。

获取当前时间,获取task时间,比较发现时间还没到,继续wait。

当时间到的时候wait返回,下次循环中,又尝试从队首获取元素,队首有元素,不阻塞,直接取出来。

四、线程池

1,线程池是什么

想象这么一个场景: 在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人, 而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来 一个任务,起一个线程进行处理的模式。

很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知 道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着 业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快 递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带 出的线程池的模式。

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

2,标准库中的线程池

        使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.

        返回值类型为 ExecutorService

        通过 ExecutorService.submit 可以注册一个任务到线程池中.

Executors 创建线程池的几种方式

        newFixedThreadPool: 创建固定线程数的线程池

        newCachedThreadPool: 创建线程数目动态增长的线程池.

        newSingleThreadExecutor: 创建只包含单个线程的线程池.

        newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定

3,实现线程池

核心操作为 submit, 将任务加入线程池中

使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.

使用一个 BlockingQueue 组织所有的任务

每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.

指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增 线程了.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Thread5 {

    //使用一个类来描述这个工作线程
    static class Worker extends Thread{
        private int id = 0;
        //每个Worker线程都需要从任务队列中取任务
        //需要获取任务队列的实例
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue,int id){
            this.queue = queue;
            this.id = id;
        }

        @Override
        public void run() {
           //此处的try把while包裹了,目的是只要线程收到异常,就立刻结束run方法(也是结束线程)
            try {
                while (!Thread.currentThread().isInterrupted()){
                    Runnable command = queue.take();
                    System.out.println("thread: " + id + "running...");
                    command.run();
                }
            }catch (InterruptedException e){
                System.out.println("线程被终止");
            }
        }
    }

    //本质就是生产者消费模型
    //调用execute的代码就是生产者,生产了任务
    //worker就是消费者,消费队列中的任务
    //交易产所就是BlockingQueue
    static class MyThreadPool{
        //阻塞队列组织若干任务
        private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
        //List组织若干线程
        private List<Worker> workers = new ArrayList<>();
        //先假设有10个线程容量
        private static final int maxWorkerCount = 10;

        //实现execute方法和shutdown方法
        public void execute(Runnable command) throws InterruptedException {
            //线程数目较少的时候,创建新的线程为工作线程
            //线程足够多,达到阈值,不用创建新线程
            if (workers.size() < maxWorkerCount){
                Worker worker = new Worker(queue,workers.size());
                worker.start();
                workers.add(worker);
            }
            queue.put(command);
        }

        //当shutdown结束,所有线程结束
        public void shutdown() throws InterruptedException {
            //终止所有线程
            for (Worker worker : workers){
                worker.interrupt();
            }
            //还需要等待每个线程执行结束
            for (Worker worker : workers){
                worker.join();
            }
        }

        static class Command implements Runnable{
            private int num;
            public Command(int num){
                this.num = num;
            }

            @Override
            public void run() {
                System.out.println("正在执行任务:" + num);
            }
        }

        public static void main(String[] args) throws InterruptedException {
            MyThreadPool myThreadPool = new MyThreadPool();
            for (int i = 0; i < 1000; i++){
                myThreadPool.execute(new Command(i));
            }
            Thread.sleep(2000);
            myThreadPool.shutdown();
            System.out.println("线程已经被销毁");
        }

    }

}

当最初创建线程池实例的时候,此时线程池中没有线程,继续调用execute的时候,就会触发创建线程的操作。

 interrupt触发异常或者修改标记位,让线程run方法结束,线程也就随之结束了,对于的jion也就结束了。

如果队列为空,take就会被阻塞,阻塞到其他线程调用execute插入元素为止

猜你喜欢

转载自blog.csdn.net/qq_50156012/article/details/123206898
今日推荐