JavaEE -> 多线程:单例模式、阻塞队列

单例模式

常见的设计方案:

1)单例模式

2)工厂模式

单例:单个实例(对象)

有些场景之下只能有一个对象:比如娶老婆是,但生孩子就不一定是单例的。

很多用于管理数据的对象应该是“单例的” : MySQL JDBC DataSource (描述了mysql服务器的位置)

然后咱们就会想,那为啥会专门有一个设计模式?

我写代码的时候只给这个类new一次对象不就可以了,但不是每一个人都这样想,于是我们就需要让编译器帮我们监督(出现多个对象时就会报错)。比如像final,interface,@override,throws

单例模式有两种:饿汉模式和懒汉模式

饿汉模式:在类加载的时候就会创建实例对象

懒汉模式:在第一次调用getInstance的时候才会创建实例对象

eg:文本编辑器(记事本)

打开一个10G的大文件,存在两种形式

1.先把所有的内容都加载到内存中,然后再显示内容(加载速度慢)------->饿汉

2.只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其它内容。------>懒汉

饿汉模式:

//饿汉模式:类一加载就会创建实例
class Singleton {
    private static Singleton instance = new Singleton();//当类被加载的时候就会创建实例

    private Singleton() {
        //将构造方法设为私有,类外面的代码都无法new出多个对象。
    }

    public static Singleton getInstance() {
        //通过这个方法可以获取到刚才创建的实例
        //后续想获取到这个实例,都通过getInstance()方法获取
        return instance;//
    }
}

public class Demo24 {
    //单例模式
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);//两个其实是对同一个对象进行操作
//        Singleton s3 = new Singleton();//上面已有一个实例,出现报错
    }
}

 懒汉模式:

//懒汉模式:当需要实例(第一次使用getinstance)的时候再去创建
class SingletonLazy {
    private static SingletonLazy  instance = null;

    private SingletonLazy() {

    }

    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

单例模式是否是线程安全的?

饿汉模式是线程安全的,只进行了读取instance这一个变量;

懒汉模式是不安全的,既读取而且还修改变量,可能会出现线程不安全问题。

那么该如何加锁呢?

那么该如何实现线程安全的同时又能不对执行效率产生影响呢?

package thread;

//懒汉模式:当需要实例(第一次使用getinstance)的时候再去创建
class SingletonLazy {
    private static volatile SingletonLazy  instance = null;

    private SingletonLazy() {

    }

    public static SingletonLazy getInstance() {
        if(instance == null) {//这个if是用来判断是否需要加锁
            synchronized(SingletonLazy.class) {
                if(instance == null) {(维护原子性)
//第二次if是为了防止破坏单例模式,因为有可能有两个线程此时都通过了第一层if
//然后现在都在竞争锁,此时当第一个线程完成实例的创建就会释放锁,但是第二个线程不知道,也会创建一个实例,那么就会破坏单例。
                    instance = new SingletonLazy();
                }
            }
        }

        return instance;
    }
}

public class Demo25 {
    public static void main(String[] args) {

    }
}

指令的重排序(编译器的优化):保证逻辑不变,调正原有代码的执行顺序,提高程序的执行效率

Question:t1执行到new的过程中,已经加锁,t2还能执行吗?t2还能穿插进来吗?(以上述懒汉模式代码为例)

t2执行的第一个if,并未涉及到加锁的操作,是可以执行的。

锁的阻塞等待一定是两个线程都加锁的时候触发。

如果t2并未满足第一个if的条件,没进入if的内部,没加锁,直接返回instance。

New对象这个操作可能会触发指令重排序

 new可拆分成三步:

1.申请内存空间

2.在内存空间上构造对象(构造方法)

3.把内存地址赋值给instance引用

执行顺序可为123或132,单线程下无影响,多线程可能有。

假设按照132执行:

当t1线程执行完1和3时,此时instance已经是非空,instance指向的是一个还未初始化的非法对象。此时还未执行2.

t2线程突然开始执行,t2判定 instance == null, 此时不成立,直接将return instance。

进一步t2线程可能会访问instance里面的属性和方法,此时就很容易出现问题(与上面还未初始化的非法对象)。

指令重排序的解决:用volatile关键字

单例模式是一个慢慢完善的过程,不是一下就能写好的!!!!

阻塞队列:多线程代码中经常用到的一种数据结构

特性:

1)线程安全

2)带有阻塞特性

    a)如果队列为空,继续出队列,就会发生阻塞。阻塞到其他线程往队里添加元素为止。

    b)如果队列为满,继续入队列,就会发生阻塞。阻塞到其它线程从队列里取走元素为止。

意义:可以用来实现“生产者消费者模型”

举例:

过年很多人一起包饺子(多个线程一起工作,比单线程效率高),由于擀面杖只有一个,所以专门有一个人负责擀面皮。

于是擀面皮的那个人就是生产者,然后把擀的面皮放在筛子(阻塞队列)里,拿面皮包饺子的人叫消费者。

生产者把生产出来的内容,放到阻塞队列中,消费者从队列中获取内容。

当擀面皮的人生产慢了,拿面皮的人就得等(队列为空,阻塞);

当擀面皮的人生产快了,擀面的人就得等,筛子装不下了。(队列为满,阻塞)

生产者消费者模型的意义:

1.解耦合:两个模块联系越紧密,耦合越高。(对于分布式系统,更加有意义)

2.削峰填谷

峰:短时间内请求量比较多

谷:短时间内请求量比较少

三峡大坝的原理:

当上游水量增加,大坝关闸蓄水,承担压力,往下游有节奏放水;

当上游水量少了,大坝开闸放水,给下游提供水。

阻塞队列的使用:

1.java标准库已经提供现成阻塞队列

2.标准库里,针对BlockingQueue提供了两种实现方式:

  1)数组

  2)链表

3.BlockingQueue虽然继承自Queue,Queue提供的方法都可以用,但这些方法都不具备“阻塞”特性

package thread;


import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

//阻塞队列
public class Demo26 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        queue.put("111");//阻塞式入队列
        queue.put("nb");
        queue.put("lihai");
        queue.put("111");

        String elem = queue.take();//阻塞式出队列
        System.out.println(elem);
        elem = queue.take();//阻塞式出队列
        System.out.println(elem);
        elem = queue.take();//阻塞式出队列
        System.out.println(elem);
    }
}

自己实现一个阻塞队列:普通队列+线程安全(加锁)+阻塞(wait和notify)

基于数组实现——>环形队列

1.普通队列:

2.线程安全:

判断条件是否需要加锁?

入队列需要让判断条件加锁,不然会出现线程不安全问题

出队列也需要让判断条件加锁

3.阻塞:wait()和notify()

还存在一个问题:

如果有两个线程同时put,现在队列是满的,A线程先阻塞,B线程也阻塞,此时有一个C线程take,把A线程唤醒,A此时可以put,A线程一路执行到下面的notify,此时B线程也会被A线程唤醒;此时队列已经满了,但B还准备往里面放元素,会造成线程安全。

解决:把if条件换成while循环,不只是判断一次;当其它线程把它唤醒之后,还会进行判断队列是否为满或者空,如果不满或非空,才可释放wait,不在等待;反之继续wait等待。

package thread;

//阻塞队列的实现

class MyBlockingQueue {
    //最大长度也可指定构造方法,由构造方法的参数决定
    String[] data = new String[100];

    private volatile int head = 0;//队列的起始位置

    private volatile int tail = 0;//队列结束位置的下一个位置

    private volatile int size = 0;//队列中有效元素的个数

//    Object locker = new Object();//可用this,也可用locker

    public void put(String elem) throws InterruptedException {
        synchronized (this) {//加锁
            while (size == data.length) {
                //队列满了,继续插入元素,应该阻塞
                this.wait();
            }
            //队列未满,添加元素
            data[tail] = elem;
            tail++;
            size++;
            if (tail == data.length) {
                //此时,如果tail已经到了数组末尾,让tail回到开头(环形队列)
                tail = 0;
            }
            this.notify();//唤醒take中的wait
        }
    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //如果队列为空,继续取元素,阻塞
                this.wait();
            }
            //队列不为空
            String tmp = data[head];//返回队头元素
            head++;//删除队头元素
            size--;
            if (head == data.length) {
                head = 0;//到了数组末尾,从0开始
            }
            this.notify();//唤醒put中的wait
            return tmp;
        }
    }

}


    public class Demo27 {
        public static void main(String[] args) {
            MyBlockingQueue queue = new MyBlockingQueue();

            //生产者,消费者,分别使用一个线程表示

            //消费者
            Thread t1 = new Thread(() -> {
                while(true) {
                    try {
                        String ret = queue.take();
                        System.out.println("消费:"+ret);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            //生产者
            Thread t2 = new Thread(() -> {
                int num = 1;
                while(true) {
                    try {
                        queue.put(num + "");
                        System.out.println("生产:"+ num);
                        num++;
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            t1.start();
            t2.start();
        }
    }

当wait被唤醒之后,队列一定是不满的吗?

不一定。除了notify,还有其它唤醒方式——> interrupt方法   ->会出现InterruptedException

于是wait唤醒之后,我们得循环进行多次判断,确认队列不满,才可往后执行;如果满,那就继续wait

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值