文章目录
案例一:线程安全的单例模式
线程安全的单例模式分为:饿汉模式 和 懒汉 模式
- 饿汉的单例模式,是比较着急的去进行创建实例的
- 懒汉的单例模式,是不太着急的去创建实例,知识在用的时候,才去正在创建
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.懒汉模式
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.实现一个线程安全的单例模式
这是一道非常经典的面试题,我们在学习这个模式的时候一定要抓住以下三点:
- 正确的位置加锁
- 双重 if 判定
- 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.生产消费者模型
生产消费者模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中
假设:有两个服务器AB,A作为入口服务器直接接收用户的网络请求,B作为应用服务器来给A提供一些数据
如果不使用生产者消费模型,此时A和B的耦合性是比较强的,在开发A代码的时候就得充分了解B提供的一些接口,开发B代码的时候也得充分了解到A是怎么调用的,一旦想把B换成C,A的代码就需要较大的改动,而且如果B挂了,也就可以直接导致A也顺带挂了
2.生产者消费者模型优点
优点1:能够让多个服务器程序之间更充分的解耦合
对于请求:A是生产者,B是消费者
对于响应:A是消费者,B是生产者
阻塞队列作为交易场所
此时,A只需要关注如何和阻塞队列交互,不需要认识B
B也只需要关注如何和阻塞队列交互,也不需要认识A(队列是不变)
如果B挂了,对于A来说没有影响,如果把B换成C,A也完全感知不到
优点2:能够对于请求进行"削峰填谷"
不使用生产者消费者模型:
使用生产者消费者模型:
3.阻塞队列的用法(代码实现)
4.生产者消费者模型(代码实现)
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(安排),参数有两个:任务是什么,多长时间之后执行
1.Timer 内部都需要什么
- 描述任务:创建一个专门的类来表示一个定时器中的任务(TimerTask)
- 组织任务(使用一定的数据结构把一些任务给放到一起)
注意:**PriorityBlockingQueue<>()**既带优先级又带有阻塞队列
- 执行时间到了的任务:需要执行时间最靠前的任务,就需要有一个线程,不停地去检查当前优先队列的队首元素,看看当前最靠前的这个任务是不是时间到了
2.上述代码存在的缺陷
如果在main函数当中运行上述代码你会发现
所以上述代码中存在两个非常的缺陷
第一个缺陷解决方法,实现一个Comparable接口去比较时间的大小:
当我们实现 Comparable 接口之后 执行代码,就能正常运行
2.第二个缺陷:
解决办法:使用 wait 机制
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.线程池的优势
总体来说,线程池有如下的优势:
- 降低资源消耗 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度 当任务到达时,任务可以不需要等待线程的创建就能立即执行
- 提高线程的可管理性 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2.线程池的构造方法
线程池的真正实现类是 ThreadPoolExecutor .其构造方法有4种,在这里我们主要强调一种:
把一个线程池,想象成是一个"公司",公司里有很多的员工在干活,把员工分成两类:1.正式员工 2.临时工
接下来我们在来看这个构造方法里面的参数代表的含义:
- int corePoolSize 核心线程数(正式员工的数量)
- int maximumPoolSize 最大线程数(正式员工+临时工)
- long keepAliveTime 允许临时工摸鱼的时间
- TimeUnit 时间的单位(s,ms,us…)
- BlockingQueue < Runnable > workQueue 任务队列,线程池会提供一个 submit 方法 让程序猿把任务注册到线程池中,加到这个任务队列中
- ThreadFactory threadFactory 线程工厂,线程是怎么创建出来的
- RejectedExecutionHandler handler 拒绝策略
3.线程池代码的实现
标准库中提供了一个简化版的线程池 Executors 本质是针对 ThreadPoolExecutor 进行了封装,提供了默认参数,看一下 Executors 是如何使用的,我们仿照这个实现一个线程池
创建个固定线程数目的线程池,遍历100次"hello threadpool"
输出结果:
创建个简单的线程池实例:
首先我们需要知道线程池里有什么
- 先能够描述任务(直接使用 Runnable)
- 需要组织任务(直接使用BlockingQueue)
- 能够描述工作线程
- 还需要组织这些线程(利用数据结构)
- 需要实现,往线程池里添加任务
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");
}
});
}
}
}
版权归原作者 青花瓷~ 所有, 如有侵权,请联系我们删除。