【操作系统】volatile、wait和notify以及“单例模式”基础知识

简介: 【操作系统】volatile、wait和notify以及“单例模式”基础知识

1.volatile关键字

  1. 内存可见性问题是在编译器优化的背景下,一个线程把内存给改了,另外一个线程不能及时感知到。为了解决这种问题就引入了volatile。
  2. volatile的存在是为了保证内存可见性,但是并不保证原子性针对一个线程读,一个线程修改,这个场景volatile合适,而针对俩个线程修改,volatile做不到!
  3. 当使用volatile来修饰变量时,编译器就不会做出只读寄存器不读内存这样的优化这个关键字只能修饰变量,没有别的用法。
  4. volatile禁止了编译器优化,避免直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。
  5. 面试中遇到volatile,多半也不会脱离JMM(Java Memory Model  java内存模型)工作内存不是真的内存,主内存才是真的内存。
  6. 站在JMM的角度来看待volatile;正常程序执行的过程中会把内存的数据先加载到工作内存中,再进行计算处理。编译器优化可能会导致不是每次都真的读取主内存,而是直接读取工作内存中的缓存数据(这就可能导致内存可见性问题)。而volatile起到的作用就是保证每次读取数据都是从工作内存上重新读取

2.wait和notify:

  1. 线程有个特别的地方,抢占式执行,线程调度的过程是随机的!而wait和notify是用来调配线程顺序的,让线程按照想要的顺序来调配,控制多线程之间的执行先后顺序。
  2. wait是Object的方法,线程执行到wait就会发生阻塞,直到另外一个线程调用notify把这个wait唤醒,才会继续往下走。wait只会影响调用的那个线程,不影响其他线程。
  3. wait操作本质上做了三件事:

1.释放当前锁

2.进行等待通知

3.满足一定条件的时候(别人调用了notify),

    wait被唤醒然后尝试重新获取锁

  1. wait等待通知的前提是要把锁释放,而释放锁的前提是你得先加了锁。没锁怎么释放!因此wait的第一步操作就是先释放锁,保证其他线程能够正常往下运行,wait和加锁操作是密不可分的。
  2. 线程1没有释放锁的话,线程2就无法调用到notify;线程1调用了wait,在wait里面就自动释放锁了,这个时候虽然线程1阻塞在synchronized里面,但是此时锁是释放状态,线程2能拿到锁。其他线程必须要上锁才能调用notify,调用了notify才会唤醒wait,但是notify所在的线程也得先释放锁,wait才会在唤醒后的第一件事就是尝试重新加锁。
  3. 要保证加锁的对象和调用wait的对象是同一个对象,还要保证调用wait的对象和调用notify的对象是同一个对象
Object object = new Object();
Thread t1 = new Thread(()->{
    synchronized(object){
        try {
            object.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
});
a.wait();
b.notify();//不能唤醒wait!
  1. 如果线程t1先执行了wait,线程t2后调用notify,此时notify会唤醒wait;但是如果线程t2先执行了notify,线程t1后调用wait,此时就错过了,特别注意即使没人调用wait,调用notify也不会有异常和副作用。
  2. 还有一个notifyAll。多个线程都在调用wait,notify是随机唤醒一个,而notifyAll则是全部唤醒,即使唤醒了所有的wait,这些wait就需要重新竞争锁,重新竞争锁的过程依然是串行的。
  3. wait和sleep的对比:

对比:

1.都是让线程进入阻塞等待的状态

2.sleep是通过时间来控制何时唤醒的

 wait是由其他线程通过notify来唤醒的

3.wait有个重载的版本,参数可以传时间,表示等待的最大时间(类似于join)

3.单例模式:

  1. 单例模式和工厂模式是常见的设计模式。
  2. 单例模式本质上借助了编程语言自身的语法特性,强行限制某个类,不能创建多个实例,有且只有一个实例。
  3. static修饰的成员(属性)变成了类成员(类属性),此时当属性变成类属性的时候此时及已经是单个实例了。更具体地说是类对象的属性,而类对象是通过JVM加载.class文件来的,其实类对象在JVM中也是单例。换句话说,JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的static修饰的成员也就只有一份
  4. 饿汉模式:
1. //饿汉模式
2. class Singletion{
3. //这个instance就是Singletion的唯一实例
4. private static Singletion instance = new Singletion();
5. 
6. //在类外可以通过getInstance来获取到实例
7. public static Singletion getInstance(){
8. return instance;
9.     }
10. 
11. //把构造方法设置为private。此时类外就无法继续调用new实例
12. private Singletion(){}
13. 
14. }
15. 
16. public class Demo8 {
17. public static void main(String[] args) {
18. //要继续使用这个实例
19. Singletion singletion = Singletion.getInstance();
20.     }
21. }
  1. 懒汉模式,比饿汉模式更加的高效:
1. //懒汉模式(bug版本)
2. class Singletion{
3. 
4. private static Singletion instance = null;
5. 
6. public static Singletion getInstance(){
7. if(instance == null){
8. //这里才是创建实例,
9. // 首次调用getInatance才会触发,后续调用就立即返回
10.             instance = new Singletion();
11.         }
12. return instance;
13.     }
14. 
15. private Singletion(){}
16. 
17. }
18. 
19. public class Demo8 {
20. public static void main(String[] args) {
21. Singletion singletion = Singletion.getInstance();
22.     }
23. }
  1. 上面写的懒汉和饿汉,谁的线程安全,谁的不安全?考虑这俩代码是否线程安全本质上是在考虑多个线程下同时调用getInstance,是否会有问题。饿汉模式中的getInstance只是单纯的读取数据的操作,不涉及修改,因此线程安全。而懒汉模式的getInstance既涉及到读又涉及到修改操作,则线程不安全。
  2. 如何修改让懒汉模式线程安全?加锁!,把多个操作打包成一个原子操作。

//解决方案 1

public static Singletion getInstance(){
    synchronized (Singletion.class){
        if(instance == null){
            instance = new Singletion();
        }
    }
    return instance;
}

//分析:这种加锁方式把线程不安全的问题解决了,但是又有了新的问题

 懒汉模式的线程不安全也只是实例创建之前(首轮调用的时候)才会触发线程不安全问题

 一旦实例创建好后,线程就安全了。导致当后续线程安全的时候仍然还得加锁,加锁开销挺大,代价大!

//解决方案 2

public static Singletion getInstance(){
    //实例创建之前,线程不安全,需要加
    //实例创建之后,线程安全,不需要加
    if(instance == null){//判断是否要加锁
        synchronized (Singletion.class){
            if(instance == null){//判断是否要创建实例
                instance = new Singletion();
            }
        }
    }
    return instance;
}
  1. 理解双重if判定,当多线程首次调用getInstance的时候,这些线程发现instance都为空,进入了外层if并且开始往下执行竞争锁,竞争成功的锁再来完成实例创建的操作;当这个实例创建好了之后,其他竞争到锁的线程就被里层的if挡住了,便不会再创建实例了。当再有第二批线程想来,直接就被挡在了外层if,直接就return了。
  2. 还有一个重要的问题,假设俩个线程同时调用getInstance,第一个线程拿到了锁,进入第二层if,开始new对象。new操作本质上分成三个步骤,先是申请内存,得到内存首地址;再调用构造方法,来初始化实例;最后把内存的首地址赋值给instance引用
  3. 这个场景下,编译器可能会进行指令重排序的优化操作,在单线程的角度下,步骤2和步骤3是可以调换顺序的(单线程的情况下,此时步骤2和步骤3谁先执行后执行效果都一样)。多线程情况下,假设此处触发了指令重排序,并且按照步骤1、3、2的顺序来执行,有可能线程t1执行了步骤1、3之后,执行步骤2之前,线程t2调用了getInstance,得到了不完全的对象,只是有内存,内存上的数据无效,这个getInstance就会认为instance非空,就直接返回了instance并且在后续可能就会针对instance进行解引用操作(使用里面的属性和方法),这就会出现异常!
  4. 这就是指令重排序带来的问题,要想解决这个问题,就是要禁止指令重排序,使用volatile,既能保证内存可见性,又能解决指令重排序的问题。
1. //懒汉模式(完整版)
2. class Singletion {
3. //加上volatile就禁止了指令重排序
4. private volatile static Singletion instance = null;
5. 
6. public static Singletion getInstance() {
7. //实例创建之前,线程不安全,需要加
8. //实例创建之后,线程安全,不需要加
9. if (instance == null) {//判断是否要加锁
10. synchronized (Singletion.class) {
11. if (instance == null) {//判断是否要创建实例
12.                     instance = new Singletion();
13.                 }
14.             }
15.         }
16. return instance;
17.     }
18. 
19. private Singletion(){}
20. }

 

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

相关文章
|
3天前
|
安全 Unix Linux
【Linux入门指南:掌握开源操作系统的基础知识】(四)
【Linux入门指南:掌握开源操作系统的基础知识】
|
3天前
|
Linux
【Linux入门指南:掌握开源操作系统的基础知识】(三)
【Linux入门指南:掌握开源操作系统的基础知识】
|
3天前
|
Java Linux Android开发
Android基础知识:解释什么是Android(安卓)操作系统?
Android基础知识:解释什么是Android(安卓)操作系统?
166 0
|
3天前
|
Unix Linux Windows
【Linux入门指南:掌握开源操作系统的基础知识】(二)
【Linux入门指南:掌握开源操作系统的基础知识】
|
3天前
|
Ubuntu Unix Linux
【Linux入门指南:掌握开源操作系统的基础知识】(一)
【Linux入门指南:掌握开源操作系统的基础知识】
【Linux入门指南:掌握开源操作系统的基础知识】(一)
|
存储 编解码 缓存
1.5微型计算机的操作系统 计算机专业理论基础知识要点整理
1.5微型计算机的操作系统 计算机专业理论基础知识要点整理
151 0
|
3天前
|
监控 Unix Linux
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
35 0
|
3天前
|
Linux
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
31 0
|
3天前
|
存储 Linux C语言
Linux:冯·诺依曼结构 & OS管理机制
Linux:冯·诺依曼结构 & OS管理机制
11 0
|
3天前
|
存储 Linux
linux查看系统版本、内核信息、操作系统类型版本
linux查看系统版本、内核信息、操作系统类型版本
64 9

热门文章

最新文章