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 queue = new LinkedBlockingQueue<>();

然后我们在认识下 BlockinQueue的两个带有阻塞特点的方法
put()阻塞式入队列和take()阻塞式出队列

了解不如实现

这里我知道了一个很有用的一种层级关系吧

第一层 了解怎么使用,了解概念
第二层 知道背后的原理
第三层 自己实现一个类似

这里我就跟着学怎么自己做一个阻塞队列

首先基于一个普通队列 加上线程安全 加上阻塞 就能实现一个基本的阻塞队列

这里我们的队列是个循环队列

有两个关键的属性吧 head tail
[head,tail)构成了区间 , 区间里的内容就是队列中的有效元素。

初始情况下 队列空的时候 headtail
但是满的时候也是head
tail

这时候分辨不出 有两个解决方案
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后的方法也具有可见性和有序性。 不过做两手准备也没问题。

posted @ 2026-08-30 13:15  曾章吉的浩吧  阅读(4)  评论(0)    收藏  举报