日升时奋斗,日落时自省
目录
1、单例模式
1.1、饿汉模式
1.2、懒汉模式
1.3、饿汉和懒汉的线程安全
2、生产者消费者模型
2.1、理论解释
2.2、优势
2.3、阻塞队列代码解析
2.4、生产者消费者代码解析
2.5、简单实现阻塞队列代码解析
3、定时器
3.1、定时器的使用
3.2、自主实现简单的定时器
单例模式:是设计模式的一种
模式如何理解,打过小游戏的友友们都知道“攻略”这个词应对不同的BOSS,其实模式也就是一种解决方法,也如下棋一样,会有棋谱如何解决不同的棋局
模式这里挺起来 有点类似于框架,仅仅是类似,因为限制不同
模式与框架的区别
(1)框架是硬性,大佬已经写好的,不按照框架来写,代码跑不起来(不得不遵守)
(2)模式是软性,可以不遵守也能跑起来,但是代码的可可读性,可维护性,可扩展性都比较低(可以不遵守)
单例如何解释:单个 实例对象 在某些情况下只能创建一个实例不应该创建多个实例,单例模式只是在这里限定,并不是说一定要用
创建一个实例,这件事情是程序员自己做的,也可以不用单例模式,自己创建一个,再也不创建了也行(这样的可以称为“君子协定”)
举一个例子解释:
在古代是可以一夫多妻,但是也有只爱一人的,只娶了一个妻子的,这就是君子协定,没有外界因素束缚,他可以娶多个,但是他以君子协定(口头协议)来约束自己(相当于程序员不用模式)
在现代是一夫一妻,只能娶一个人,不只是君子协定,还有法律要求(使用模式)
使用单例模式后,想在创建多个实例都难
单例模式,就是针对上述的需求场景进行了更强制的保证,通过巧用java的现有语法,达成了某个类,只能被创建出一个实例,这样的效果(创建多个实例就报错提醒)
之前的博客中写到JDBC数据库连接,DataSource这个就很适合使用单例模式,为什么呢?
因为只需要创建一个数据源,单例模式不是正好嘛
实现单例模式有很多种方式,这里介绍最常见的两种方式(饿汉模式和懒汉模式)
为什么叫做饿汉:这里说的是单例模式,创建一个实例的先后来定义的名字,饿汉在类加载阶段创建实例,以饿汉定名
类加载阶段:运行一个java程序,就需要让java进程能够找到并读取对应的.class文件,就会读取文件内容,并解析,构造类对象...这一系列的过程操作称为 类加载(有点像预处理)
class Singleton{ //本身就是安全的, 因为只读就不会有线程安全问题
//创建一个实例 ,这就是饿汉模式,为什么有static来修饰,是为了能够与类联系起来
//static使这里的属性具有唯一性
private static Singleton instance=new Singleton();
//如果需要使用这个唯一的实例,统一通过Singleton.getInstance() 方式进行来获取
public static Singleton getInstance(){ //静态方法是为了在main直接能够掉用
return instance;
}
//为了避免Singleton 类不小心被复制出来多份
//把构造方法设置为private 在类外就不能在通过new的方式创建出来Singleton类来创建
private Singleton(){
}
}
public class ThreadDemo_Single {
public static void main(String[] args) {
// Singleton s=new Singleton(); //这里就会报错哦
Singleton s2=Singleton.getInstance();
Singleton s1=Singleton.getInstance();
System.out.println(s1==s2);
}
}
这里我们先附一下代码,解释当前代码
私有化的构造方法 :如果在外面创建了一个实例化对象(看一下效果)
这里能看到报错的结果,因为私有化构造方法的限制,保证了只能通过调用方法的形式进行操作实例化对象。
private static Singleton instance=new Singleton();
这个属性和实例无关,而是和类相关
java代码中每个类,都会在编译完成后得到.class文件,JVM运动时就会加载这个.class文件读取其中的二进制指令,并且在内存中构造出对应的类对象(Singleton.class)
由于类对象 在java进程里,只是有唯一一份的,因为类对象内部的 类属性也是唯一一份了
为什么叫做懒汉:以为在一开始并没有创建对象,等有人来调用方法的时候,如果没有创建对象,在进行创建;(代码大体上和饿汉模式有几分相似)
那懒汉模式好嘛,当然嘛,懒汉不是说是贬义,而是褒义,效率更胜,懒在程序和代码里能提高效率,人因为懒才发明了计算机,为了便捷,才发明了更多方便的东西,才有现在的科技
那友友们觉得谁是安全的呢?
饿汉线程更安全,为什么这么说?
下面来看一下懒汉模式为啥不安全:
那我们来加个锁,看一下对不对
那我们知道要加锁,那如何进行加锁才能解决问题
那现在线程安全问题也解决了,那是不是就安全,其实还有其他问题,内存可见性问题
假设有很多线程,都去进行getInstance,这个时候,是否就会有编译优化的风险,只有第一次读是真正的读内存,后续都是读寄存器内存可见性问题
也就是说每次在寄存器可能读的都是instance==null,仍然会有问题,相当于加锁没有起效
另外,还会涉及到指令重排序问题
instance=new Singleton();
拆分成三个步骤:
1、申请内存空间
2、调用构造方法,把这个内存空间初始化成一个合理的对象
3、把内存空间的地址赋值给instance引用
正常情况下,123按顺序走,编译器为了提升效率也可能调整代码执行顺序(单线程无所谓)
如果是多线程环境,t1按照132步骤执行的话,13刚刚执行结束,CPU就把t2调度来执行,t2就相当于直接返回instance引用并且可能会尝试使用引用中的属性。
但是t1的2还没有执行完,t2拿到了非法的对象,还没构造完成的不完整对象
volatile解决内存可见性问题
(1)解决内存可见性
(2)禁止指令重排序
下面是附的完整版代码(解决线程问题,解决内存可见性问题,解决指令重排序问题)
class SingletonLazy{ //可以写的就会出现线程安全问题
//static 使所有的属性具有唯一性 ,懒汉模式就是不会直接创建对象,比较懒,但是懒有懒得好处,它节省资源
private volatile static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){ //如果满足创建对象条件,就互加锁,
synchronized (SingletonLazy.class) {
if(instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
}
//单例模式防止 在外面的类中new 实例化
private SingletonLazy(){
}
}
public class ThreadDemo_Single2 {
public static void main(String[] args) {
SingletonLazy s=SingletonLazy.getInstance();
SingletonLazy s1=SingletonLazy.getInstance();
System.out.println(s1==s);
}
}
这个模型一下解释不清楚:在学习这个模型之前先要知道阻塞队列
阻塞队列也是一个队列(数据结构)
先进先出 是队列的一个特点 但是不是所有队列都遵循这个规则
数据结构中的优先级队列(PriorityQueue)
还有就是这里要提到的阻塞队列,虽然是先进先出,但是同时存在其他的特点(顾名思义)
阻塞(针对多线程,如果是单线程就没有实际意义了):
(1)如果队列为空了,执行出队列的操作,就会阻塞等待,阻塞到另一个线程往队列中添加元素(队列不为空),阻塞结束当前线程继续执行
(2)如果队列满了,执行入队列的操作,就会阻塞等待,阻塞到另一个线程从队列取走元素(队列不满),阻塞结束当前线程继续执行
这里提一下消息队列:也是特殊队列,因为它就是在阻塞队列基础上进行的,加上一个“消息的类型”按照制定类别进行先进先出,此时就构成了一个消息队列,更是一个数据结构,消息队列的应用比较频繁,也被单独实现成了一个程序,该程序可以通过网络的方式和其他程序进行通信,单独部署到一组服务器上,存储能力和转发能力都大大提升了。
以上提及的消息队列也成了一个组件“中间件”
rabbit mq就是一种,还有active mq,rocket mq,kafka都是,因为都是消息队列的应用所以,使用大同小异。
基于阻塞队列的特性,可以实现“生产者和消费者模型”
什么是生产者消费者模型?
简单的举一个例子:
卖货这个流程 简介一下
(1)就是生产 出来 消费者买,生产出来的货正常量,但是恰逢过年大家买年货消费者就多,一购而空,这时候消费者还想要卖货就要等着生产商这边再产货才行,所以需要就阻塞等待.
(2)过年过完了,但是当时生产者囤货又囤的太多了,此时的生产量太大,消费者少了,这下生产者就需要等待了
其实这个例子与前面的阻塞队列在我理解以来是同一个意思。
1、实现了发送方和接收方之间的“解耦”
注:解耦就是降低耦合的过程(写代码尽量能够让代码低耦合 、高内聚)
低耦合: 就是将代码模块之间的联系降低 防止一个个方面的代码出现问题导致整个代码都瘫痪
高内聚:就是将代码元素之间的联系程度提高(如 在找衣服的时候,衣服就如元素,衣柜就如代模块,每一个人的衣服都应该在规定的衣柜里就容易找,这就高内聚,不同衣柜就是模块之间低耦合)
在实现使用项目中,服务器之间相互调用可以看出(图解)
没有使用“生产者和消费者模型” 时
使用模型的情况:
总结:生产者和消费者模型降低了耦合性,同时可以在极端条件下阻塞进程,减少代码带来不必要的崩坏,在A服务器受损时不会影响到服务器B和C,如果B或者C服务器崩坏也不会影响服务器A。
2、生产者消费者模型 ,第二个可以做到“削峰填谷”,保证系统的稳定性
简单表示一下波峰填谷:
在这里就是阻塞的意思:为了让整个服务器维持平衡,如果充值的数据量一次性暴增,阻塞队列就会在一定数据的时候进行阻塞,阻塞队列中的数据再在通过其他服务器慢慢取出,不会导致服务器直接崩溃,这里的阻塞等待相当于提供了一个缓冲的作用
注:在大型的项目上很实用,因为数据量是不可预知的,服务区要撑得住,所以阻塞队列就是可以满足该情况,就像如果某大型平台突然停止运行,就会导致同类小型的竞争平台数据量暴增导致,如果没有做好措施,服务器,平台,或者网页就可能崩溃,阻塞队列就是解决这种突发数据量增长问题。
阻塞队列在java标准库中使用是BlockingQueue<> 创建对象有数组(ArrayBlockingQueue)、链表(LinkedBlockingQueue)和堆(PriorityBlockingQueue)类型的。
他们是队列自然也包含队列的基本方法offer(),peek(),poll()但是不具有阻塞功能
阻塞功能的主要存在于 put 入队列 take 出队列 这两个方法
以下代码可以尝试如果队列中空时,出队列会进行等待,程序并不会结束,当然这里表现的并不明显,之后,在写一个简单的生产者消费者模型,观察这种情况
public class BlockQueue {
//阻塞队列
public static void main(String[] args) throws InterruptedException {
//阻塞队列 也分为三种不同的队列构成, 数组 链表 和 堆
BlockingQueue blockingQueue=new LinkedBlockingQueue<>();
/*put 是入队的
* take 是出队
* */
blockingQueue.put("hello");
String res=blockingQueue.take();
System.out.println(res);
res=blockingQueue.take();
//产生越界不会进行出问题 会进行等待 观察后可以看出 运行并没有结束
System.out.println(res);
}
}
大体思路:由多线程构造出该模型,满足生产元素空时 消费者阻塞等待,消费者满足时 生产者阻塞等待
首先:创建一个阻塞队列 下来才能满足阻塞等待这个条件,这里给阻塞队列设置的是8个空间,便于测试生产速度如果快的话,后来仍然会与消费者的速度同步进行,因为会阻塞队列会进行阻塞
BlockingQueue blockingQueue=new LinkedBlockingQueue<>(8);
接下来创建先生产者或者消费者都行(代码上附加简单注释)
这里先写生产者 线程 便于大家理解
//便于理解先创生产者 线程
Thread producer=new Thread(()->{
int count=0; //这里就简单记为 生产者生产的数据
while(true){
try {
//将生产的数据 放入 阻塞队列中
blockingQueue.put(count);
//这里就做一个标识作为验证
System.out.println("生成元素 :"+ count);
Thread.sleep(500);
count++; //表示新数据
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start(); //线程执行
后写消费者的线程
//创建消费者
Thread customer =new Thread(()->{
while(true){
try {
//将对阻塞队列的数据进行提取
Integer result=blockingQueue.take();
//打印出来检测作为验证
System.out.println("消费元素 :" + result);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
上述代码中生产者线程的代码Thread.sleep(500);没有删除,这里展示一下删除后的运行结果,友友们可以自己试试
阻塞队列也需要先写一个普通的队列,可以是链表来写,也可以是数组来写,阻塞队列更像是循环队列,我们这里就以数组的形式进行实现,两个指针,一个头指针,一个尾指针
循环队列写出来,基础就打好了,但是线程调度是随机的,既然写多线程就会涉及到线程安全问题。阻塞队列的细节:
(1)这里有关++的都会涉及多线程安全问题,所以直接整个循环队列都套上一个锁。
(2)原来判满条件是不能返回的,现在需要的阻塞 所以把return 换成this.wait();
(3)既然有wait就 需要notify唤醒所以在notify唤醒都是在出队列和入队列的最后,出队列时为空就会阻塞,需要入队一次,队列不为空,此时唤醒才行,同理,入队列时为队列线程就会在此阻塞,需要出队一次,队列不为满,此时唤醒
(4)上面三个问题阻塞队列已经基本完成,但是有一个多线程中存在的缺陷,就是多线程调度的时候是可以切换的,所以判满或者判空被唤醒以后可能仍然为空或者为满,但是单单就一个if语句是不能连续判断当前队列是满的还是空的,所以if需要改成while
class MYBlockingQueue{
private int[] items=new int[100];
private int head=0;
private int tail=0;
private int size=0;
//入队列的 代码 线程安全 离不开锁 所以在以下的操作中需要用到锁的地方很多 ,所以当前位置都需要这些东西,直接给整体都加上一个锁
public void put(int vaule) throws InterruptedException {
synchronized (this){
while (size==items.length){ //防止在过程中 被唤醒后仍然是 满的
this.wait();// (1)
}
items[tail] = vaule;
tail++;
//这里为什么不会写成 tail = (tail + 1)%itmes.length
// 因为当前值已经进行了加加 不在需要进行加1的 操作了
if(tail>=items.length){
tail=0;
}
size++;
this.notify();// 唤醒(2)
}
}
//出队列
public Integer take() throws InterruptedException {
int result=0;
synchronized (this){
while (size==0){ //防止在过程中 被唤醒后仍然是 空的
this.wait(); //(2)
}
result=items[head];
head++;
if(head>=items.length){
head=0;
}
size--;
this.notify(); //唤醒 (1)
}
return result;
}
}
上面附的就是阻塞队列的简单实现了,可以自己用一下main函数进行测试一下生产者消费者模型。
一种就是 指定特点时刻 提醒
另一种是 指定特定时间段之后 提醒
这里提及的定时器不是提醒什么,而是执行一个实现准备好的方法或者代码
这个也是咱们开发中一个常用的组件,尤其是网络编程的时候,很容易卡连不上,但是要及时止损,不能一直卡也不说是什么问题,让客户一直等吧。
这里java标准库里也给我们提供了定时器(Timer)
这里粗略使用一下定时器,然后简单实现一个计时器(稍微有一点点繁琐,但是不要有心理负担)
主要使用的是 schedule这个方法其中有两个参数包含了 任务 和 时间
public class TimerTest {
//简单的了解 定时器的使用凡是
public static void main(String[] args) {
System.out.println("启动程序");
Timer timer=new Timer(); //定时器 java标准库定义
//使用方法是 schedule 包含 有两个参数 第一个是任务 第二个是 时间 单位是ms
//这里每个任务的时间都是不一样的, 因为只有主线程一个线程在跑这些方法,所以会按照时间最短的先跑
//所以先跑 的是任务3 然后是任务 2 最后是 任务1
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务 1");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时任务 2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时任务 3");
}
},1000);
}
}
定时器是前台线程,不会因为主线程结束,而运行结束所以这里的运行结果能明显看出运行的红色方块。
思路:
(1)让被注册的任务,能够在指定事件,被执行
单独在定时器内部写一个线程,让着个线程周期性的扫描,判定任务是否到时间了,如果到时间了就执行,没到时间,就再等等。
(2)一个定时器是可以注册N个任务的,N个任务会被按照最初约定的时间,按顺序执行
但是这个顺序就需要点东西了,这里用的是阻塞队列的一种堆的阻塞队列
(1)有一个扫描线程,负责判定时间/执行任务
(2)还要有一个数据结构,来保存所有被注册的任务
使用一个优先级队列来表示,但是在多线程中就需要注意线程安全,可以加锁来解决线程安全问题,此处还有个选择,标准库提供的PriorityBlockingQueue
一个类来实现定时器 首先要有一个自定义类来为阻塞队列做一个准备,因为这里需要任务 和 时间。
class MyTask{
private Runnable runnable; //定义一个任务
private long time; //定义一个时间
public MyTask (Runnable runnable, long time){
this.runnable=runnable;
this.time=time;
}
//获取当前时间
public long getTime(){
return time;
}
public void run(){
runnable.run();
}
/* @Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}*/
}
第一步完成了,后面就先定义好一个类来写定时器,定时器的需要一个扫描线程,和一个前面提到的阻塞队列(以堆的形式),所以下面定义好了一个空线程,和一个阻塞队列,但是阻塞队列需要时间去比较时间不是吗,所以这里就在里面写了一个比较器,用来比较时间,谁的时间小就放在堆顶,这里就相当于是一个小根堆
//定时器 需要 一个 执行方法 和 一个扫描线程
private Thread t=null;
//有一个阻塞优先级队列 ,来保存任务
private PriorityBlockingQueue queue=new PriorityBlockingQueue<>(8,new Comparator(){
@Override
public int compare(MyTask o1, MyTask o2) {
return (int) (o1.getTime()- o2.getTime());
}
});
那接下来写什么呢,那就是构造方法了,指定两个参数,一个是任务 第二个是 时间,但是步骤稍微多了那么一点点(这里有一个阻塞等待,现在不说为什么,先写剩余的代码,后面在解释wait的作用)
public MyTimer(){
t=new Thread(()->{
while(true){
try {
//取出首元素 检查队列首元素是否满足时间条件
//如果时间没到 就把任务塞回去
//另外时间到了就正常执行
synchronized (this){
MyTask myTask=queue.take();
Long curTime=System.currentTimeMillis();
//如果时间大于了我们当前的时间 就再把当前时间装进队列里
if(curTime
扫描线程已经写完了,思路的第一个步骤也算是走完了,但是第二步骤就是用一个方法将任务装进队列中,
//指定两个参数
//第一就参数是 任务 内容
//第二参数 是 任务执行多 少秒
public void schedule(Runnable runnable,long after){
MyTask task=new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(task);
synchronized (this){
this.notify();
}
}
这里遗留了一个问题,其实是两个问题,为什么要在线程上加锁,为什么要用wait等待。
(1)先解决wait等待问题,前面的博客中提到wait是可以有自己的时间限制的,因为如果时间不到,不能一直执行if语句从队列中弹出,再塞回去也是要有消耗的呀,所以并不能单单的用if语句再这里解决问题,所以这里使用wait确定一个等待时间自动开始,或者有新任务入队列,就会进行一次notify唤醒,从新计算等待时间
(2)然后就是加锁问题,因为线程是会进行随机调度的,所以可能线程在计算wait等待时间前就已经调度走了,在这样的情况下入队列是一个最小时间,那再回来的时候wait会进行更新吗,答案是当然不会,因为我们之前已经take出队列了,所以wait等待的时间是上一次的时间,所以加锁的目的就是为了能够保证wait能够执行完,notify唤醒通知不会被放鸽子,同时也不会导致新任务添加入队列时wait时间计算错误