当前位置: 首页 > article >正文

多线程相关案例

目录

1. 单例模式

1) 饿汉模式

2) 懒汉模式

2. 阻塞队列

1) 阻塞队列的特性

2) 模拟实现阻塞队列

3. 定时器

4. 线程池

1) ThreadPoolExecutor 类

2) 模拟实现线程池


1. 单例模式

单例模式是最经典的设计模式之一。

单例模式,顾名思义,就是这个类在整个程序中只能有一个实例。

具体来说就是,约定一个类,只能有一个对象,通过编码技巧,让编译器强制进行检查,提前在类里把对象给创建好,然后将构造方法设为 private。

单例模式有两种实现方式,一种是饿汉模式,一种是懒汉模式。

懒就是,非必要,不采取行动,举个例子,在早上你可以一直睡觉,直到上学快要迟到了,你再起床,这样做的话,就能休息时间最大化,保证今天的学习效率。

而饿的话,就是截然相反,早上就非常早起床,然后马上去上学,起太早的话,可能就会睡眠不足,导致上课的时候昏昏欲睡,学习效率低。

1) 饿汉模式

我们可以具体来实现一下单例模式,用饿汉模式的方式,这个比较简单。

因为是单例模式嘛,而考虑到是饿汉的方式来实现,我们就可以不管三七二十一,先提前创建好对象,等到有人想要获取对象的时候,就直接返回创建好的对象,但是只做这些还不足以让它成为单例模式,我们还需要将它的构造方法设置成私有的,并且要将获取对象的方法设置成 public,并且用 static 修饰,这样就可以通过 类名.方法名 来调用获取实例的方法了。

最后写出来就是这个样子的:

// 期望这个类,能够有唯一实例
class Singleton {
    private static final Singleton instance = new Singleton();

    // 通过这个方法获取到刚才的实例
    // 后续如果想使用这个类的实例,就都通过 getInstance 方法来获取
    public static Singleton getInstance() {
        return instance;
    }

    // 把构造方法设为 private,此时类外面的其他代码,就无法 new 出这个类的对象了
    private Singleton() {
    }
}

2) 懒汉模式

接下来再来看看单例模式如何用懒汉的方式来实现。

懒,就是非必要,不采取行动。

那我们就可以先不初始化对象,等到有人调用获取对象的方法的时候,再来创建对象,然后再返回创建好的对象,那整个代码就跟饿汉方式差不多,就是延迟了创建对象的时机。

写出来就是这样的。 

这下修改完代码后,就才真正没问题了。

class SingletonLazy {
    private static volatile SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        // 是否是首次创建对象(从而判断要不要加锁)
        if (instance == null) {
            // 保证串行化执行,不会 new 多个对象
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {

    }
}

饿汉方式实现的单例模式会涉及到线程安全的问题,需要注意三点:

1. 加锁,保证 if 条件和 new 操作是原子的,只会创建出一个对象。

2. 两层 if,第一个 if 用来判断是否要加锁,第二个 if 用来判断是否要创建对象。

3. volatile,用来禁止指令重排序问题。

而饿汉模式天然就是线程安全的,因为它只涉及到了多个线程的读取操作。

2. 阻塞队列

阻塞队列是多线程代码中常用到的一种数据结构。

1) 阻塞队列的特性

阻塞队列的特性有两个:

1. 线程安全

2. 带有阻塞特性

     a) 当队列为空时,从队列中取元素的话,就会阻塞,阻塞到其他线程往队列里添加元素为止

     b) 当队列为满时,往队列中放元素的话,就会阻塞,阻塞到其他线程从队列中取走元素为止

阻塞队列最大的意义,就是用来实现 "生产者消费者模型"(是一种常见的编码方式)。

怎么理解生产者消费者模型呢?举个例子:

那么为什么要使用生产者消费者模型呢?生产者消费者模型的优点是什么?

生产者消费者模型的优点:

1. 解耦合

两个模块,联系越紧密,耦合就越高。

2. 削峰填谷

峰:短时间内请求量比较多

谷:请求量比较少

在 Java 标准库里,提供了现成的阻塞队列 BlockingQueue,来让我们使用。

标准库里,针对于阻塞队列提供了两种最主要的实现方式: 1. 基于链表  2. 基于数组

虽然 BlockingQueue 继承了 Queue,但是不建议在 BlockingQueue 里使用 Queue 的方法,因为这些方法都不具备 "阻塞" 特性。

put 方法是阻塞式的入队列,take 方法是阻塞式的出队列。

2) 模拟实现阻塞队列

了解完上面这些,我们自己也可以模拟实现一个阻塞队列,阻塞队列就是比普通队列多了线程安全以及阻塞特性,那我们就可以先实现一个队列(只用实现 put 和 take 方法),然后再加上线程安全(synchronized),再加上阻塞就好了(wait & notify)。

首先,普通队列有两种实现方式,一种是基于数组,另一种是基于链表。

基于链表实现的话,就是头插尾删,蛮简单的,

基于数组实现的话,就是要实现一个循环数组。

这里我们使用数组来实现好了,添加元素就是往 tail 下标处添加元素,删除元素就是删除 head 处元素,直接 head++ 即可。

有了上面基础,我们就可以直接开始敲代码了,只用实现 put 和 take 方法就行。

首先搭好框架,然后再来开始写代码。

// 不写作泛型了,直接让这个队列里面存储字符串
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) {
        
    }

    public String take() {
        
    }
}

大致写好了普通队列,接下来再保证线程安全,首先是涉及到要修改变量的操作,就得加锁,因为两个方法大部分都有修改操作,那我们就可以直接创建个 Object 对象,然后加锁把方法包裹起来。

然后也要注意内存可见性和指令重排序的问题,以防万一,给那几个变量加上 volatile 会比较好。

最后再加上阻塞,使用 wait & notify 来完成就好。

官方文档建议,在使用 wait 的时候,最好搭配 while 来使用(将 if 换成 while)

完整代码:

// 不写作泛型了,直接让这个队列里面存储字符串
class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,也可以构造方法的参数来判定
    private String[] data = new String[1000];

    private volatile int head = 0;// 队列的起始位置
    private volatile int tail = 0;// 队列的结束位置的下一个位置
    private volatile int size = 0;// 记录当前队列有效元素个数
    private Object locker = new Object();

    // 提供核心方法,入队列和出队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            // 首先判断,队列满不满
            while (size == data.length) {
                // 阻塞, 等到有其他线程取元素的时候,再唤醒
                locker.wait();
            }
            // 队列没满,就真正的往里面放元素
            data[tail] = elem;
            tail = (tail + 1) % data.length;
            size++;
            locker.notify();// 这个 notify 用来唤醒 take 的 wait
        }

    }

    public String take() throws InterruptedException {
        synchronized (locker) {
            while (size == 0) {
                // 队列为空,阻塞,等到后面有其他线程添加元素后再唤醒
                locker.wait();
            }
            // 队列不空,就需要把 head 位置的元素给删除掉,并且返回
            String ret = data[head];
            head = (head + 1) % data.length;
            size--;
            // 这个 notify 用来唤醒 put 的 wait
            locker.notify();
            return ret;
        }
    }
}

借助这个阻塞队列,我们就可以实现一个简单的生产者消费者模型,就是一个线程往里面添加元素,另一个线程从里面消费元素。

// 不写作泛型了,直接让这个队列里面存储字符串
class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,也可以构造方法的参数来判定
    private String[] data = new String[1000];

    private volatile int head = 0;// 队列的起始位置
    private volatile int tail = 0;// 队列的结束位置的下一个位置
    private volatile int size = 0;// 记录当前队列有效元素个数
    private Object locker = new Object();

    // 提供核心方法,入队列和出队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            // 首先判断,队列满不满
            while (size == data.length) {
                // 阻塞, 等到有其他线程取元素的时候,再唤醒
                locker.wait();
            }
            data[tail] = elem;
            tail = (tail + 1) % data.length;
            size++;
            locker.notify();
        }

    }

    public String take() throws InterruptedException {
        synchronized (locker) {
            while (size == 0) {
                // 队列为空,阻塞,等到后面有其他线程添加元素后再唤醒
                locker.wait();
            }
            String ret = data[head];
            head = (head + 1) % data.length;
            size--;
            locker.notify();
            return ret;
        }
    }
}


public class Demo24 {
    public static void main(String[] args) {
        // 生产者,消费者,分别使用一个线程表示。(也可以使用多个线程)
        MyBlockingQueue queue = new MyBlockingQueue();

        // 消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    String num = queue.take();
                    System.out.println("消费元素:" + num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });
        // 生产者
        Thread t2 = new Thread(() -> {
            int num = 1;
            while (true) {
                try {
                    queue.put(num + "");
                    System.out.println("生产元素:" + num);
                    num++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果,发现生产者一下子把队列生产满了,后面就是消费者消费一个元素,生产者生产一个元素。

3. 定时器

定时器是一个日常开发的常见组件。

约定一个时间,等时间到了之后,就会执行某个代码逻辑。

这个就跟闹钟差不多。

定时器非常常见,尤其是在进行网络通信的时候。

举个例子:

而 Java 标准库也给我们提供现成的定时器 Timer 类,来让我们使用。

// 定时器
public class Demo25 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 给定时器安排了一个任务,预定在 xxx 时间去执行。
        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. 需要创建一个类,来描述任务(必须包括时间和任务)

那该用哪个数据结构来组织任务对象呢?

定时器跟时间有关,并且时间短的任务先执行,那我们就可以使用优先级队列来组织任务对象。

根据这些,我们就可以敲代码了,首先创建出两个类 MyTimer 和 MyTimerTask,分别用来表示定时器和任务,MyTimer 里需要存放任务列表,所以还需要添加一个优先级队列 queue(记得要传比较器),而 MyTimerTask 里包含了时间和任务,并为它们提供构造方法,MyTimer 构造方法就需要创建出一个线程,用来扫描任务,还有一个 schedule 方法,用来添加任务。

// 模拟实现一个简单的定时器
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
        @Override
        public int compare(MyTimerTask o1, MyTimerTask o2) {
            // 时间小的先执行,建立小根堆
            return (int) (o1.getTime() - o2.getTime());
        }
    });

    public MyTimer() {
        Thread thread = new Thread(() -> {
            // 写扫描线程的逻辑
        });
        // 不要忘记启动线程
        thread.start();
    }

    public void schedule(MyTimerTask task) {
        queue.offer(task);
    }
}

class MyTimerTask {
    private long time;
    private Runnable runnable;

    public MyTimerTask(long time, Runnable runnable) {
        // 这里记录绝对时间方便我们计算
        this.time = time + System.currentTimeMillis();
        this.runnable = runnable;
    }

    // 提供 getter 方法
    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}

然后再来实现扫描线程的工作。

这个其实也很简单,

先判断队列是否为空,如果队列为空,就需要阻塞等待,直到有线程调用 schedule 方法,

不空的话,只要不停地看一下任务队列里的队首元素,看一下该元素的时间到没到,到了的话就执行任务,执行完任务后将任务从队列里弹出,

没到的话,就等时间到了再来执行任务,(也可以看做是懒,非必要,不采取行动)

而前面也说了,要让线程阻塞等待,那就可以使用 wait,而使用 wait 就必须得先加锁,此时,我们发现其实要进行阻塞的这几处,都有修改操作,schedule 是要往队列里面添加新任务,而扫描线程扫描任务列表,当时间到了,就得执行任务,执行完任务后就得将任务出队,那这样的话,我们加锁和使用 wait 就是顺理成章的事情了。

写完后就是这样的:

// 模拟实现一个简单的定时器
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
        @Override
        public int compare(MyTimerTask o1, MyTimerTask o2) {
            // 时间小的先执行,建立小根堆
            return (int) (o1.getTime() - o2.getTime());
        }
    });

    public MyTimer() {
        Thread thread = new Thread(() -> {
            synchronized (this) {
                // 写扫描线程的逻辑
                while (true) {
                    // 首先判断队列为不为空,空的话,就阻塞等待
                    // 直到有线程调用 schedule 方法为止
                    while (queue.isEmpty()) {
                        // 要加锁
                        // 阻塞等待
                        try {
                            this.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask task = queue.peek();
                    if (System.currentTimeMillis() >= task.getTime()) {
                        // 时间到了,需要执行任务, 然后出队列
                        task.getRunnable().run();
                        queue.poll();
                    } else {
                        // 没到时间的话,就进行等待
                        try {
                            // 如果添加了新的任务,也需要将线程唤醒
                            // 重新更新一下,最早的任务是什么,以及更新等待时间
                            this.wait(task.getTime() - System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        // 最后要记得启动线程
        thread.start();
    }

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

class MyTimerTask {
    private long time;
    private Runnable runnable;

    public MyTimerTask(long time, Runnable runnable) {
        // 这里记录绝对时间方便我们计算
        this.time = time + System.currentTimeMillis();
        this.runnable = runnable;
    }

    // 提供 getter 方法
    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}

写完之后,我们就可以来测试一下,看看代码是否正确,就写个例子,调用下方法就行。

没什么问题,一个简单的定时器就写好了

4. 线程池

线程诞生的意义,是因为进程太重量。

而线程因为创建/销毁的时候不需要申请/释放资源,所以比进程快,线程也叫做轻量级进程。

但是,如果频繁的创建销毁线程,此时这个开销也是不容忽视的。

此时,就有两种提高效率的方法,第一种:协程。第二种:线程池。

协程也被叫做轻量级线程,它相比于线程,把系统调度的过程,给省去了,就是让我们自己来调度

但是很遗憾,在 Java 协程的圈子里,很少有人会用协程。

主要有两个原因:第一,Java 官方没有实现协程,虽然有第三方库实现了,但是不够权威也不够靠谱。    第二,Java 有线程池,有线程池兜着底,让线程也不至于太慢。

那到底什么是线程池呢?

可以先看看,这里面的池是什么意思。

池其实是计算机中一种重要的思想方法,很多地方都会涉及到(如线程池、进程池、内存池、连接池)。

举个例子来说明吧:

那么线程池,就是在使用第一个线程的时候,提前把线程 2、3、4、5....给创建出来,

如果后续想要使用新的线程,不用重新创建,而是直接从线程池里面拿就好,就能降低创建线程的开销。

Java 标准库提供了写好的线程池,来让我们使用。

1) ThreadPoolExecutor 类

ThreadPoolExecutor 类的功能非常丰富,提供了很多参数,上述标准库的几个方法,就是给这个类填写了不同的参数用来构造线程池。

我们也可以来学学这个类(面试会考)。

ThreadPoolExecutor 的核心方法就两个:1. 构造(构造方法参数很多)      2. 注册任务(添加任务)

我们来看看它的构造方法,直接看最下面的构造方法就行,因为这个的参数涵盖了上面的参数。

(有一说一,第一次看到这么多参数的构造方法,天都要塌了,但是理解之后其实还好)

2) 模拟实现线程池

了解以上这些后,我们可以自己来实现一个线程池。

线程池:写一个固定线程数目的线程池(暂时不考虑线程的增加和减少)

(1) 提供构造方法,指定创建多少个线程

(2) 在构造方法中,把这些线程都创建好

(3) 有一个阻塞队列,能够用来存放要执行的任务

(4) 提供 submit 方法,用来添加任务

有了以上思路,就很好写代码了。

// 模拟实现一个简单的线程池
class MyThreadPool {
    
    // 将创建好的线程放在数组里面,等到需要使用的时候就拿出来用
    private List<Thread> threadList = new ArrayList<>();
    // 拒绝策略就是阻塞等待,直到别的线程使用 submit 方法添加任务为止
    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(() -> {
                // 让每个线程不停的从队列中消费任务,如果没有任务了,
                // 那此时线程就会阻塞等待,直到有其他线程调用 submit 方法为止
                while (true) {
                    // 让这个线程,从队列中消费任务,并进行执行
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
            threadList.add(t);
        }
    }
}

写个简单的代码来测试一下:

没啥问题。


http://www.kler.cn/a/418692.html

相关文章:

  • Attention显存统计与分析
  • Sybase数据恢复—Sybase数据库无法启动,Sybase Central连接报错的处理案例
  • 【数据结构】二叉搜索树(二叉排序树)
  • MySQL - 性能优化
  • 【北京迅为】iTOP-4412全能版使用手册-第二十五章 进程间通信简介
  • Vue项目Docker部署
  • 文本内容处理命令和正则表达式
  • 使用springBoot的freemarker生成按模板生成word
  • pycharm(一)安装
  • electron学习 渲染进程与主进程通信
  • ArrayList和LinkedList的区别(详解)
  • Mybatis:CRUD数据操作之多条件查询及动态SQL
  • 基于RISC-V 的代理内核实验(使用ub虚拟机安装基本环境)
  • Vivado程序固化到Flash
  • 「Mac畅玩鸿蒙与硬件34」UI互动应用篇11 - 颜色选择器
  • 【VUE3】【Naive UI】<NCard> 标签
  • Redis 3 种特殊数据类型详解
  • 详解Qt 之QSwipeGesture手势滑动
  • unity中:Unity 中异步与协程结合实现线程阻塞的http数据请求
  • OGRE 3D----2. QGRE + QQuickView
  • 【博主推荐】C#中winfrom开发常用技术点收集
  • 如何在 Ubuntu 16.04 上使用 GitLab CI 设置持续集成流水线
  • 基于ZYNQ-7000系列的FPGA学习笔记3——开发环境搭建点亮一个LED
  • 1.2 算法和算法评价
  • 计算机网络之传输层协议UDP
  • com.intellij.diagnostic.PluginException……[Plugin: hg4idea]