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

1.wait和notify(都需要搭配synchronized使用)

  join(),哪个线程调用这个方法,哪个线程就堵塞

  wait和notify都是Object的方法,随便定义一个对象都可以使用

wait():

执行之前要做三件事:

1.释放当前的锁

2.让线程进入阻塞状态

3.当线程被唤醒的时候,重新获取到锁。

//wait()
public class Demo22 {
    public static void main(String[] args) throws InterruptedException {
//        Object object = new Object();
//        System.out.println("wait之前:");
//        object.wait();//释放锁的前提是得有锁,不然会产生不合法监视器状态异常
//        System.out.println("wait之后: ");


        Object object = new Object();
        System.out.println("wait之前:");
        synchronized(object) {
            object.wait();
            //把 wait 要放到 synchronized 里面来调用. 保证确实是拿到锁了的.
            //wait() 先释放锁,再让线程进入阻塞状态,最后等待唤醒重新获取到锁
        }
        System.out.println("wait之后: ");

    }
}

wait和notify():可以用来避免"线程饿死"

package thread;

public class Demo23 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                System.out.println("wait之前:");
                try {
                    object.wait(1000);//可以添加等待的时间
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait之后: ");
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object) {//可重复锁
                System.out.println("进行通知:");
                object.notify();//唤醒,如果有多个线程需要唤醒,可以用notifyAll(),不过notify更为可控,唤醒之后,各个线程重新获取锁的过程是串行执行的
            }

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

2.单例模式:

常见的设计方案:

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是用来判断是都需要new对象
                    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关键字

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

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

特性:

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)

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

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、付费专栏及课程。

余额充值