首页 > 其他分享 >多线程的案例

多线程的案例

时间:2024-04-02 15:58:36浏览次数:32  
标签:Singleton instance 队列 案例 线程 new 多线程 public

目录

1.单例模式

1.1 饿汉模式

1.2懒汉模式-单线程版

1.3懒汉模式-多线程版

1.3懒汉模式-多线程版(改进)

2.阻塞队列

2.1阻塞队列是什么

2.2生产者消费模型

2.3标准库中的阻塞队列

2.4阻塞队列实现

3.定时器

3.1定时器是什么

3.2标准库中的定时器

3.3实现定时器

4.线程池

4.1线程池是什么

4.2标准库中的线程池

4.3实现线程池

5.总结-保证线程安全的思路


1.单例模式

单例模式是校招中最常考的设计模式之一。

什么是设计模式?

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

软件开发中,也有一些固定的套路。

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

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

单例模式具体的实现方式有很多,最常见的是“饿汉”和“懒汉”两种。

1.1 饿汉模式

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

class Singleton {
 private static Singleton instance = new Singleton();
 private Singleton() {}
 public static Singleton getInstance() {
 return instance;
 }
}
1.2懒汉模式-单线程版

类加载的时候不创建实例,第一次使用的时候才能创建实例。

class Singleton {
private static Singleton instance = null;
 private Singleton() {}
 public static Singleton getInstance() {
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}
1.3懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的。

线程安全问题发生在首次创建实例时,如果多个线程同时调用getInstance方法,就可能导致创建出多个实例。

一旦实例已经创建好了,后面再多的线程环境调用getInstance就不再有线程安全问题了(不再修改instance了)

加上synchronized可以改善这里的线程安全问题。

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

1.3懒汉模式-多线程版(改进)

以下代码在加锁的基础上,做出了进一步行动:

  • 使用双重 if 判定,降低锁竞争的频率。
  • 给instance加上了volatile。
class Singleton {
 private static volatile Singleton instance = null;
 private Singleton() {}
 public static Singleton getInstance() {
 if (instance == null) {
 synchronized (Singleton.class) {
 if (instance == null) {
 instance = new Singleton();
 }
 }
 }
 return instance;
 }
}

理解双重if判定 / volatile:

加锁/解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此,后续使用的时候,就不必进行加锁了。

外层的 if 就是判定下看当前是否已经把instance实例创建出来了。

同时为了避免“内存可见性”导致读取的instance出现偏差,于是补充上volatile。

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

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

1.有三个线程,开始执行getInstance,通过外层的 if (instance == null)知道了实例还没有创建的消息,于是开始竞争同一把锁。

2.其中线程1率先获取到锁,此时线程1通过里层的 if(instance == null )进一步确认实例石是否已经创建,如果没创建,就是把这个实例创建出来。

3.当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的if (instance == null)来确认实例是否已经创建,发现实例已经创建出来了,就不在创建了。

4.后续的进程,不必加锁,直接就通过外层 if (instance == null)就知道实例已经创建了,从而不再尝试获取锁了,降低了开销。

2.阻塞队列

2.1阻塞队列是什么

阻塞队列是一种特殊的队列,也遵守“先进先出”的原则。

阻塞队列能是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列里插入元素。

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

2.2生产者消费模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是要直接从阻塞队列里取。

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。(削峰填谷)
  2. 阻塞队列也能使生产者和消费者直接解耦。
2.3标准库中的阻塞队列

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

  • BlockingQueue是一个接口,真正实现的类是LinkedBlockingQueue。
  • put方法用于阻塞式的入队列,take用于阻塞式的出队列。
  • BlockingQueue也有offer,poll,peek等方法,但是这些方法不带有阻塞特性。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// ⼊队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();

生产者消费者模型

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();
}
2.4阻塞队列实现
  • 通过“循环队列”的方式来实现
  • 使用synchronized进行加锁控制
  • put插入元素的时候,判定如果队列满了,就进行wait。(注意:要在循环中进行wait,被唤醒时不一定队列就满了,因为同时可能是唤醒了多个线程)
  • take取出元素的时候,判定如果队列为空,就进行wait(也就是循环wait)
public class BlockingQueue {
 private int[] items = new int[1000];
 private volatile int size = 0;
 private volatile int head = 0;
 private volatile int tail = 0;
 public void put(int value) throws InterruptedException {
 synchronized (this) {
 // 此处最好使⽤ while.
 // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
 // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了
 // 就只能继续等待
 while (size == items.length) {
 wait();
 }
 items[tail] = value;
 tail = (tail + 1) % items.length;
 size++;
notifyAll();
 }
 }
 public int take() throws InterruptedException {
 int ret = 0;
 synchronized (this) {
 while (size == 0) {
 wait();
 }
 ret = items[head];
 head = (head + 1) % items.length;
 size--;
 notifyAll();
 }
 return ret;
 }
 public synchronized int size() {
 return size;
 }
 // 测试代码
public static void main(String[] args) throws InterruptedException {
 BlockingQueue blockingQueue = new BlockingQueue();
 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 {
 blockingQueue.put(random.nextInt(10000));
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
}, "⽣产者");
 producer.start();
 customer.join();
 producer.join();
 }
}

3.定时器

3.1定时器是什么

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

3.2标准库中的定时器
  • 标准库中提供了一个Timer类,Timer类的核心方法为schedule。
  • schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
 @Override
 public void run() {
 System.out.println("hello");
 }
}, 3000);
3.3实现定时器

定时器的构成

  • 一个带优先级队列(不要使用PriorityBlockingQueue,容易死锁)
  • 队列中的每个元素是一个task对象
  • Task中带有一个时间属性,队首元素就是即将要执行的任务
  • 同时有一个worker线程一直扫描队首元素,看队首元素是否需要执行

1.Timer类提供的核心接口为Schedule,用于注册一个任务,并指定这个任务多长时间后执行。

public class MyTimer {
 public void schedule(Runnable command, long after) {
 // TODO
 }
}

2.Task类用于描述一个任务(作为Timer的内部类),里面包含了一个Runnable对象和一个time(毫秒时间戳)

这个对象需要放到优先队列中,因此需要实现Comparable接口。

class MyTask implements Comparable<MyTask> {
 public Runnable runnable;
 // 为了⽅便后续判定, 使⽤绝对的时间戳.
 public long time;
 public MyTask(Runnable runnable, long delay) {
 this.runnable = runnable;
// 取当前时刻的时间戳 + delay, 作为该任务实际执⾏的时间戳
 this.time = System.currentTimeMillis() + delay;
 }
 @Override
 public int compareTo(MyTask o) {
 // 这样的写法意味着每次取出的是时间最⼩的元素.
 // 到底是谁减谁?? 俺也记不住!!! 随便写⼀个, 执⾏下, 看看效果~~
 return (int)(this.time - o.time);
 }
}

3.Timer实例中,通过PriorityQueue来组织若干个Task对象

通过Schedule来往队列中插入了一个个对象

class MyTimer {
 // 核⼼结构
 private PriorityQueue<MyTask> queue = new PriorityQueue<>();
 // 创建⼀个锁对象
 private Object locker = new Object();
 public void schedule(Runnable command, long after) {
 // 根据参数, 构造 MyTask, 插⼊队列即可.
 synchronized (locker) {
 MyTask myTask = new MyTask(runnable, delay);
 queue.offer(myTask);
 locker.notify();
 }
 } 
}

4.Timer类中存在一个worker线程,一直不停的扫描队首元素,看看是否能执行这个1任务。

所谓“能执行”指的是该任务设定时间已经到达了

// 在这⾥构造线程, 负责执⾏具体任务了.
public MyTimer() {
 Thread t = new Thread(() -> {
 while (true) {
 try {
synchronized (locker) {
 // 阻塞队列, 只有阻塞的⼊队列和阻塞的出队列, 没有阻塞的查看队⾸元素.
 while (queue.isEmpty()) {
 locker.wait();
 }
 MyTask myTask = queue.peek();
 long curTime = System.currentTimeMillis();
 if (curTime >= myTask.time) {
 // 时间到了, 可以执⾏任务了
 queue.poll();
 myTask.runnable.run();
 } else {
 // 时间还没到
 locker.wait(myTask.time - curTime);
 }
 }
} catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 });
 t.start();
}

4.线程池

4.1线程池是什么

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

4.2标准库中的线程池
  • 使用Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池。
  • 返回值类型为ExecutorService
  • 通过ExecutorService.submit可以注册一个任务到线程池中
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
 @Override
 public void run() {
 System.out.println("hello");
 }
});

Executors创建线程池的几种方式

  • newFixedThreadPool:创建固定线程数的线程池。
  • newCachedThreadPool:创建线程数目动态增长的线程池。
  • newSingleThreadExecutor:创建只包含单个线程的线程池。
  • newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行指令,是进阶版的Timer

Executors本质上是ThreadPoolExecutor类的封装。

4.3实现线程池
  • 核心操作为submit,将任务加入到线程池中
  • 使用Worker类描述一个工作线程,使用Runnable描述一个任务。
  • 使用一个BlockingQueue组织所有的任务
  • 每个Worker线程要做的事情:不停地从BlockingQueue中取任务并执行
  • 指定一个线程池中的最大线程数maxWorkerCount;当前线程数超过了这个最大值时,就不再新增线程了。
class MyThreadPool {
 private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 // 通过这个⽅法, 来把任务添加到线程池中.
 public void submit(Runnable runnable) throws InterruptedException {
 queue.put(runnable);
 }
 // n 表⽰线程池⾥有⼏个线程.
 // 创建了⼀个固定数量的线程池.
 public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
 Thread t = new Thread(() -> {
 while (true) {
 try {
 // 取出任务, 并执⾏~~
 Runnable runnable = queue.take();
 runnable.run();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 });
 t.start();
 }
}
}
// 线程池
public class Demo {
 public static void main(String[] args) throws InterruptedException {
 MyThreadPool pool = new MyThreadPool(4);
 for (int i = 0; i < 1000; i++) {
 pool.submit(new Runnable() {
 @Override
 public void run() {
 // 要执⾏的⼯作
 System.out.println(Thread.currentThread().getName() + " hell
 }
 });
 }
 }
}


5.总结-保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的类型。(不需要写共享资源的模型;使用不可变的对象)
  3. 直面线程安全(重点)(保证原子性;保证顺序性;保证可见性)

标签:Singleton,instance,队列,案例,线程,new,多线程,public
From: https://blog.csdn.net/2301_79719531/article/details/137206350

相关文章

  • 层次式架构案例
                  ......
  • golang中GORM使用 many2many 多对多关联查询-详细案例
    表结构和数据user表CREATETABLE`user`(`id`bigint(20)NOTNULL,`user_key`bigint(20)NOTNULL,`account`char(32)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;为了测试将user_key和id写入同样的值数据:+----+----------+---------+|id|user_k......
  • 软件设计师-案例分析-复习指导(第一部分)
    文章目录一、案例分析概述1、备考复习2、考试大纲二、考点及解题技巧1、结构化分析设计2、数据库分析设计3、面向对象分析设计4、算法分析设计5、面向对象程序设计一、案例分析概述1、备考复习下午软件设计学习说明:1、首先必须认真学习下午专题文章,文章里的讲解......
  • Linux ntsysv命令教程:如何配置运行级别服务(附案例详解和注意事项)
    Linuxntsysv命令介绍ntsysv(NetworkSysV)是一个命令行应用程序,它提供了一个简单的文本用户界面来配置在选定的运行级别中要启动的服务。这个工具显示了可用服务的列表(来自/etc/rc.d/init.d/目录的服务)以及它们的当前状态和描述。Linuxntsysv命令适用的Linux版本ntsysv命......
  • 【wu-acw-client 使用】案例
    wu-acw-client使用项目介绍,使用acw-client,创建对应Java项目的增删改查(ORM:LazyORM、mybatis),项目模块架构:mvc、feign、ddd演示项目环境:idea、mac、mysql、jdk17springboot3.0.7稳定版本1.2.3-JDK17第一步通过idea创建一个项目选择通过springInitializr创建......
  • R语言分段回归数据分析案例报告
    原文链接: http://tecdat.cn/?p=3805原文出处:拓端数据部落公众号 我们在这里讨论所谓的“分段线性回归模型”,因为它们利用包含虚拟变量的交互项。读取数据  data=read.csv("artificial-cover.csv")查看部分数据  head(data)##   tree.covershurb.gr......
  • 线性回归和时间序列分析北京房价影响因素可视化案例
    全文链接:http://tecdat.cn/?p=21467最近我们被客户要求撰写关于北京房价的研究报告,包括一些图形和统计输出。在本文中,房价有关的数据可能反映了中国近年来的变化目的人们得到更多的资源(薪水),期望有更好的房子人口众多独生子女政策:如何影响房子的几何结构?更多的卧室,更多的空......
  • Worker 进行多线程任务开发
    概念介绍在OpenHarmony中,UI线程负责处理UI事件和用户交互,而Worker线程用于处理耗时操作,以提高应用程序的响应速度和用户体验。Worker线程是与主线程并行的独立线程,通常用于执行后台任务。需要注意的是,Worker线程中不能直接修改UI元素,UI更新必须在UI线程中进......
  • 字典案例
    #案例1:#假设,已知小明、小红、小亮三人的语文、数学、英语三科成绩,将姓名、学科、成绩做对应,并计算谁的总分最高  #案例2:#假设,已知小明、小红、小亮三人的语文、数学、英语三科成绩,将姓名、学科、成绩做对应,并计算谁的总分最高  ......
  • WEB应用案例
                 ......