【JavaEE】wait/notify 及 单例模式

目录

 

🚩一、wait/notify

1.概念

2.wait()方法

3.notify()方法

4.小结

5.wait 和 sleep 对比

⛳二、单例模式

1.单例模式介绍

2.饿汉模式

3.懒汉模式

4.线程安全及解决

5.小结——经典面试题


 

🚩一、wait/notify

1.概念

wait:发现条件不满足/时机不成熟,就先阻塞等待

notify:其他线程构造了一个成熟的条件,就可以唤醒

❗❗wait 和 notify 是 Object 的方法。只要是个类对象(不是内置类型/基本数据类型),都是可以使用 wait/notify

2.wait()方法

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");

        object.wait();
        System.out.println("wait 之后");
    }
}

运行结果:

wait 之前
Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at Threading.ThreadDemo1.main(ThreadDemo1.java:15)

IllegalMonitorStateException——————Illegal 非法的——————Monitor 监视器/显示器,此处指 synchronized 监视器锁——————State 状态——————非法的锁状态异常——如果锁没有获取到,就尝试解锁,就会产生异常

✨此时object.wait();——————主要做三件事

1️⃣解锁   2️⃣阻塞等待   3️⃣当收到通知的时候就唤醒,同时尝试重新获取锁

所以 wait 必须写到 synchronized 代码块里边(才能获取锁得状态),wait必须先加锁才能解锁

正确写法:

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");
        synchronized (object) {
            object.wait();
        }
        System.out.println("wait 之后");
    }
}
其中

        synchronized (object) {
            object.wait();                ======》 加锁的对象必须是和 wait 的对象是同一个
        } 


同理 notify 也要放在 synchronized 中使用

3.notify()方法

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("wait 开始");
                synchronized (locker) {
                    locker.wait();
                }
                System.out.println("wait 结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        t2.start();
    }
}

//结果:
//wait 开始
//notify 开始
//notify 结束
//wait 结束

🔎分析:t1 先执行,执行到 wait 了,就阻塞了,1s之后 t2 开始执行,执行到 notify 就会通知 t1 线程唤醒(注意,notify是在 synchronized 内部,就需要 t2 释放了锁,t1 才能继续往下走

上述代码中,虽然 t1 是先执行的,但是可以通过 wait notify 控制让 t2 先执行一些逻辑。t2 执行完之后,notify 唤醒 t1,t1 再继续往下执行

4.小结

1️⃣使用 wait,阻塞等待会让线程进入 WAITING 状态

2️⃣wait 也提供了一个带参数的版本,参数指定的是最大等待时间

3️⃣不带参数的 wait 是死等,带参数的 wait 就会等最大时间之后,还没有通知,就自己唤醒自己

 

此处可以联想 joinwait/notify 区别
join 只能是让 t2 线程先执行完,再继续执行 t1,一定是串行的

wait/notify,可以让 t2 执行完一部分,再让 t1 执行...t1 执行一部分再让 t2 执行...t2 再执行一部分,让 t1 执行

4️⃣唤醒操作,还有一个 notifyAll, 可以有多个线程,等待同一个对象

比如在 t1 t2 t3 中都调用 object.wait ,此时在 main 中调用了 object.notify ——————会随机唤醒上述的一个线程(另外两个仍然是 waiting 状态)

如果是调用了 object.notifyAll, 此时就会把上述三个线程都唤醒,此时这三个线程就会重新竞争锁,然后依次执行

5.wait 和 sleep 对比

wait 有一个带参数的版本,用来体现超时时间,这个时候感觉好像和 sleep 差不多

✅相同点:wait 也能提前唤醒           sleep 也是能提前唤醒

❎最大的区别是初心不同:wait 解决的是 线程之间的顺序控制       sleep 单纯是让当前线程休眠一会

❎实现/使用上也是有明显的区别:wait 要搭配锁使用        sleep 不需要

⛳二、单例模式

1.单例模式介绍

📖单例模式,是一种经典的设计模式,在校招中最乐意考的设计模式之一(1.单例模式  2.工厂模式)

单例==》 单个实例(instance)  —— 对象 —— 类的实例,就是对象 

单例——一个程序中,某个类 只能创建出一个实例(一个对象),不能创建多个对象

java 中的单例模式,借助 java 语法,保证某个类,只能创建出一个实例,而不能 new 多次

在 java 语法中,如何做出单例模式的实现?有很多写法

主要介绍:

1️⃣饿汉模式(急迫)

2️⃣懒汉模式(从容)

举例子:打开一个硬盘上的文件,读取文件内容,并显示出来

饿汉:把文件所有内容都读到内存中,并显示

懒汉:只把文件读一小部分,把当前屏幕填充上,如果用户翻页了,再读其他文件内容,如果不翻页,就省下了

假设文件非常大 10G !!   饿汉方式,文件打开可能卡半天,内存够不够都不知道     懒汉方式就可以快速打开

2.饿汉模式

//把这个类设定成单例
class Singleton {
    //唯一实例的本体
    private static Singleton instance = new Singleton();//被 static 修饰,该属性是类的属性(类对象上),
    // JVM中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份    instance就是唯一的实例
    //private 是类外不能使用,这个 new 是在类里边

    //获取到实例的方法
    public static Singleton getInstance() {
        return instance;
    }

    //禁止外部 new 实例
    private  Singleton() { }

    //在类内部把实例创建好,同时禁止外部重新创建实例,此时就可以保证单例的特性了
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        //此时 s1 和 s2 是同一个对象
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        //Singleton s3 = new Singleton();//此处需要把 new 操作给禁止掉,把该类的构造方法设为 private 即可

    }
}

3.懒汉模式

核心:非必要,不创建

class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy() {  }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);//true
    }

}

4.线程安全及解决

🙈上述两种代码是否是线程安全的?

1️⃣饿汉模式安全的,调用 getInstance,只是单纯的读操作,不是修改

f6fb008b0bfe42d088c4948876e984fb.png

2️⃣ 懒汉模式不安全的,调用 getInstance,这个时候就涉及到了修改

instance = new SingletonLazy();//多线程下,懒汉模式可能无法保证创建对象的唯一性

3675478a63b44996b8fe6e6ffe649b51.png

🙈那么如何解决上述线程安全问题?


🙉加锁——保证 判定 与 new 是一个原子操作

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        //加锁————保证 判定 与 new 是一个原子操作
        synchronized (SingletonLazy.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

    private SingletonLazy() {  }
}

加锁————其实是一个比较低效的操作!!(加锁就可能涉及到阻塞等待),所以非必要,不加锁

synchronized (SingletonLazy.class);//在这里,任何时候调用 getInstance 都会触发锁的竞争,都可能会阻塞

✨其实,此处的线程不安全,只出现在首次创建对象这里,一旦对象 new 好了,后续调用 getInstance ,就只是单纯的读操作,就没有线程安全问题,就没必要再加锁了

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        //这个条件,判定是否需要加锁,如果对象已经有了,就不需要加锁,此时本省就是线程安全的
        if (instance == null) {
            //加锁————保证 判定 与 new 是一个原子操作
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {  }
}

❗❗这里注意的是:

以上代码中两个条件一样,但是需要注意的是它们的初心,目的不同

第一个条件:判定是否枷锁

第二个条件:是否要创建对象

这两个代码最大的不同就是执行时机差别很大————中间相隔加锁(synchronized),而加锁可能会导致阻塞,啥时候解除阻塞,不清楚;如果时间间隔很长,等解锁的时候说不定第二个线程已经创建好了,此时就不进入第二个判定条件了

注意❗❗❗❗这个代码现在还有一个很重要的问题——指令重排序

b5d270a0c78d4813b8d899ce3155a3d2.png

5bfe2148f6994683ac0e62fbd3479472.png

解决—— volatile ——禁止指令重排序

class SingletonLazy {
    volatile private static SingletonLazy instance = null;//禁止指令重排序

    public static SingletonLazy getInstance() {
        //这个条件,判定是否需要加锁,如果对象已经有了,就不需要加锁,此时本省就是线程安全的
        if (instance == null) {
            //加锁————保证 判定 与 new 是一个原子操作
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {  }
}

5.小结——经典面试题

单例模式——线程安全问题

1.饿汉模式——天然安全,只是读操作

2.懒汉模式——不安全,有读也有写
    1) 加锁,把 if 和 new 变成原子操作
    2) 双重 if,减少不必要的加锁操作
    3) 使用 volatile 禁止指令重排序,保证后续线程肯定拿到的是完整对象

 

 

 

 

 

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗小温

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值