8/19 Java学习博客
对于这个单例模式 里面 的懒汉模式 那个加锁后 再继续调用 getInstance()会重复上锁 造成不必要的消耗 做了一个解决。
这个解决就是 在外层 在来一次 if判断
if (instance == null) {//这两个if 执行时机差异很大
synchronized(SingletonLazy.class){//这里加锁就可能会产生阻塞 阻塞过程中可能这里的instance就发生改变了
//所以再加一个if进行判断 是否创建出来
//第一个if是否加锁 第二个if来判断是否new对象
if(instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
解决这个之后,其实目前我们还是会遭遇线程安全问题。
那这个就是由于指令重排序引起的。
我们new 一个对象出来 其实分为3步
1.开辟内存
2.在内存上构造对象
3.将内存地址给我们这个instance引用对象
指令重排序 在这里的体现 就是 2和3其实是可以对调的 结果是不影响
那么这里就会遇到个问题假设 线程1 他是先执行 将内存地址给我们这个instance引用对象 这时候他因为某种原因阻塞了。
线程2调用了这个getInsatnce()
这时候 因为线程1依旧将一个内存地址给我们这个instance了,此时他是一个未初始化的非法对象,然后同时他不满足instance == null
这是 就会直接return instance
此时我们引用对象他就不是我们想要的对象。
为了防止这个问题, 我们也是通过volatile关键字来防止重排序
这时也可以看出 volatile关键字是由 可见性 还有 有序性的
synchronized 他则是还多了一个原子性
多线程中比较常用的数据结构 : 阻塞队列
他是一种特殊队列
1.线程安全
2.带有阻塞性
(a)如果队列为空,继续出队列就会发生阻塞,阻塞到其他线程往队列里面添加元素为止.
(b)如果队列满了,继续入队列也会发生阻塞,阻塞到其他线程从队列里面取走元素为止。
提起队列还有个 生产者消费者模型
在阻塞队列里面
生产者,把生产出来的内容,放到阻塞队列中.消费者,就会从阻塞队列中获取内容(当把阻塞队列封装成单独的服务器程序部署到特定的机器上,就可被称为消息队列)
假设有个普通的A和B服务器直接连接
每个A收到的请求都会立即发给B
A有多少访问量,B也是相同的
这是就会有问题,因为每个服务器之间的对于请求消耗的资源都不一样。
就容易导致出问题 A能承担 B挂了
这时候引入阻塞队列这个中间件
起到了两个作用
1.解耦
2.削峰填谷
这里说一下第二点
当我引入阻塞队列后,A收到的大量请求,会把对应的请求写入队列,B仍然可以按之前的节奏来处理请求。
此时就起到了削峰的作用
过了峰值之后,A的请求量恢复正常,B就可以逐渐的把积压的请求处理掉,这是填谷。
有了这样的机制之后,就可以保证在突发情况来临的时候,整个服务器系统仍然可以正确执行.
java标准库 就提供了现成的阻塞队列
在标准库中针对BlockinQueue提供了两种主要实现方式
1.基于数组
2.基于链表
由于BlockinQueue是个接口 所以我们只能new他的实现类来创建对象。
这里我们就基于链表
BlockingQueue
然后我们在认识下 BlockinQueue的两个带有阻塞特点的方法
put()阻塞式入队列和take()阻塞式出队列
了解不如实现
这里我知道了一个很有用的一种层级关系吧
第一层 了解怎么使用,了解概念
第二层 知道背后的原理
第三层 自己实现一个类似
这里我就跟着学怎么自己做一个阻塞队列
首先基于一个普通队列 加上线程安全 加上阻塞 就能实现一个基本的阻塞队列
这里我们的队列是个循环队列
有两个关键的属性吧 head tail
[head,tail)构成了区间 , 区间里的内容就是队列中的有效元素。
初始情况下 队列空的时候 headtail
但是满的时候也是headtail
这时候分辨不出 有两个解决方案
1.浪费一个格子让tail指向head前一个格子就算满了
2.就是用一个size 来表示元素个数,size为0则空 , 为数组长度最大值就是满
那开始实现的时候,我们就是基于第二种方式来实现
首先 来构造我们队列的数组长度
private String[] data = new String[1000];
然后开始写出队列起始head 和队列结束位置的下一个位置tail还有个size有效元素个数
然后开始写put方法
public void put(String elem) {
if(size == data.length) {
//队列满了
//继续插入元素直接return
reutrn;
}
//队列没满
data[tail] = elem;
tail++;
//如果tail自增之后,到达了数组末尾,这个时候就需要让他回到开头(环形队列)
if (tail == data.length) {
tail = 0;
}
size++;
}
和take方法
public String take() {
if (size==0){
//队列空了
//对于普通队列,没有元素则直接return
return null;
}
//队列不空 就可以把队首元素删除掉,并进行返回;
String ret = data[head];
head++;
if(head==data.length){
head=0;
}
size--;
return ret;
}
然后我们发现 此时基本方法 他不是线程安全的 所以我们先加一把synchronized锁在两个方法
还有就是没实现阻塞 所以我们把队列满时候return 和队列空时候return null 都给改为 this.wait();进行阻塞
有阻塞还有唤醒 所以 在两个方法都在结尾加上this.notify()
put中的notify唤醒take中的wait()
take中的notify唤醒put中的wait()
那 除了notify可以唤醒wait()(无参数 死等)还有没有其他方法
有,就是interrupt 不过他会抛出异常
但是如果我们用的是try-catch 这样出现异常,方法不会结束,就可能会出现bug
比如
if(size == data.length) {
//队列满了
//如果是普通队列,继续插入元素就会阻塞
this.wait();
}
//队列没满
data[tail] = elem;
tail++;
//如果tail自增之后,到达了数组末尾,这个时候就需要让他回到开头(环形队列)
if (tail == data.length) {
tail = 0;
}
size++;
this.notify();//这个notify用来唤醒take中的wait()
}
我们这边换成try-catch后 wait抛出异常后 就接着往下走 数组满了之后 继续走 就会导致数据的一个覆盖还有size超过数组最大值
所以我们最好要判断清wait()是由什么唤醒的
然后我们就想到了被interrupt唤醒后的线程 那是不是还有继续判定是否满足条件 if(size == data.length)或者 if (size==0) ,然后我们就会发现每次 wait()都会有这种情况,
所以我们直接把if改成while()
我们原本是期望wait返回之后,再判定一次 条件仍然成立 如果不成立就继续wait
所以就改成while()天然的符合我们逻辑
使用wait 的时候,往往都是使用while作为条件判定的方式.目的就是为了让wait唤醒之后还能再确认一次,是否条件仍然满足~~
所以最后的
实现就是
public void put(String elem) throws InterruptedException {
synchronized(this) {
while (size == data.length) {
//队列满了
//如果是普通队列,继续插入元素就会阻塞
this.wait();
}
//队列没满
data[tail] = elem;
tail++;
//如果tail自增之后,到达了数组末尾,这个时候就需要让他回到开头(环形队列)
if (tail == data.length) {
tail = 0;
}
size++;
this.notify();//这个notify用来唤醒take中的wait()
}
}
public String take() throws InterruptedException {
synchronized(this) {
while (size==0){
//队列空了
//对于普通队列,没有元素则会阻塞
this.wait();
}
//队列不空 就可以把队首元素删除掉,并进行返回;
String ret = data[head];
head++;
if(head==data.length){
head=0;
}
size--;
this.notify();//用来唤醒put中的wait()
return ret;
}
}
为了避免内存可见性问题,把volatile都加好 不过我认为目前代码有点多余了,因为synchronized后的方法也具有可见性和有序性。 不过做两手准备也没问题。

浙公网安备 33010602011771号