多线程的案例

目录

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. 直面线程安全(重点)(保证原子性;保证顺序性;保证可见性)

  • 33
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值