0
点赞
收藏
分享

微信扫一扫

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



文章目录

  • ​​案例一:线程安全的单例模式​​
  • ​​1.饿汉模式​​
  • ​​2.懒汉模式​​
  • ​​3.实现一个线程安全的单例模式​​
  • ​​案例二:阻塞队列​​
  • ​​1.生产消费者模型​​
  • ​​2.生产者消费者模型优点​​
  • ​​3.阻塞队列的用法(代码实现)​​
  • ​​4.生产者消费者模型(代码实现)​​
  • ​​案列三:定时器​​
  • ​​1.Timer 内部都需要什么​​
  • ​​2.上述代码存在的缺陷​​
  • ​​3.完整代码实现​​
  • ​​案例四:线程池​​
  • ​​1.线程池的优势​​
  • ​​2.线程池的构造方法​​
  • ​​3.线程池代码的实现​​

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


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


  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.懒汉模式

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

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.实现一个线程安全的单例模式

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

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

  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. 产生阻塞效果:
  3. 如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止

    如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止

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

1.生产消费者模型


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


假设:有两个服务器AB,A作为入口服务器直接接收用户的网络请求,B作为应用服务器来给A提供一些数据

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

如果不使用生产者消费模型,此时A和B的耦合性是比较强的,在开发A代码的时候就得充分了解B提供的一些接口,开发B代码的时候也得充分了解到A是怎么调用的,一旦想把B换成C,A的代码就需要较大的改动,而且如果B挂了,也就可以直接导致A也顺带挂了

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


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


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

对于请求:A是生产者,B是消费者

对于响应:A是消费者,B是生产者

阻塞队列作为交易场所

此时,A只需要关注如何和阻塞队列交互,不需要认识B

B也只需要关注如何和阻塞队列交互,也不需要认识A(队列是不变)

如果B挂了,对于A来说没有影响,如果把B换成C,A也完全感知不到


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


不使用生产者消费者模型:

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

使用生产者消费者模型:

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

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

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

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

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

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(安排),参数有两个:任务是什么,多长时间之后执行


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

1.Timer 内部都需要什么


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


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


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


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

注意:PriorityBlockingQueue<>() 既带优先级又带有阻塞队列

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


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


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

2.上述代码存在的缺陷


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


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


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


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

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

当我们实现 Comparable 接口之后 执行代码,就能正常运行

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

2.第二个缺陷:

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

解决办法:使用 wait 机制

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

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

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种,在这里我们主要强调一种:
手把手教你如何拿捏多线程编程四大案例_线程池_21


把一个线程池,想象成是一个"公司",公司里有很多的员工在干活,把员工分成两类: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"

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

输出结果:

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

创建个简单的线程池实例:


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

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


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

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

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

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");
}
});
}
}
}



举报

相关推荐

0 条评论