内存可见性、指令重排序、wait和notify

导读:本篇文章讲解 内存可见性、指令重排序、wait和notify,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、内存可见性

1.1 引出

class Counter{
    public int flag = 0;
}

public class ThreadDemo {
    public static void main(String[] args){
        
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while(counter.flag == 0){
                // 这个循环体咱们就空着
            }
            System.out.println("t1 循环结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            counter.flag = sc.nextInt();
        });

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

在这里插入图片描述
实际上:
在这里插入图片描述
在这里插入图片描述
这种情况就叫作”内存可见性问题”。

1.2 问题分析

这个代码是不是bug?是!这也是一个线程不安全问题~ 一个线程读,一个线程修改

在这里插入图片描述
因为CPU针对寄存器的操作,要比内存操作快很多:3-4个数量级!因此load执行速度比cmp慢很多!!!
(计算机对于内存的操作,比硬盘快3-4个数量级)

在这里插入图片描述
在这里插入图片描述

就算编译器感知到了,但是因为线程是抢占式执行,随机调度的,所以并不知道什么时候会调度到修改变量的那部分代码,导致误判!
所以不是感知不到,而是不准确~ 多线程执行顺序相当复杂,编译器很难在编译阶段做出提前的预判!
在这里插入图片描述
编译器优化是广义的概念!不仅仅是编译器在做优化,也可能是操作系统和CPU在做优化~~

1.3 解决:volatile关键字

怎么解决呢?
此时就需要程序猿手动干预了。可以给flag这个变量加上 volatile关键字!意思就是告诉编译器这个变量是”易变”的,你一定要每次都重新读取这个变量的内存内容,因为指不定啥时候就变了,可不敢再进行激进的优化了~~

class Counter{
    volatile public int flag = 0;  // 或 public volatile int flag = 0;
}

此时运行代码:
在这里插入图片描述
达到预期!!!

注意:
1)但是相对的,提高了准确性但是折损了速度!快和准往往不可兼得~~
2)volatile只能修饰变量
3)volatile不保证原子性,原子性是靠synchronized来保证的。synchronized和volatile都能保证线程安全,但是是在两种不同的线程安全问题情况!
4)
在这里插入图片描述
5)
在这里插入图片描述
6)
在这里插入图片描述

1.4 补充知识点

1)Java内存模型 (JMM)

在这里插入图片描述

2)缓存 cache

在这里插入图片描述
在这里插入图片描述

二、指令重排序

我们在实现单例模式 (“懒汉模式”)时会遇到指令重排序问题:
在这里插入图片描述
怎么解决???
使用volatile!!!
volatile 有两个功能:1.解决内存可见性问题;2.禁止指令重排序

代码完全体:
在这里插入图片描述

三、wait和notify

3.1 引出

线程最大的问题就是抢占式执行、随机调度! 由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。

但是实际开发中有时候我们希望合理协调多个线程之间的执行先后顺序!
程序猿发明了一些办法来控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以通过一些API让线程主动阻塞,主动放弃CPU (给别的线程让路)~

比如,t1 t2俩线程,希望t1先干活,干的差不多了再让t2来干,就可以让t2先wait (阻塞,主动放弃CPU);等t1干的差不多了,再通过notify通知 t2,把 t2唤醒让 t2接着干。

那么上述场景,使用join或者sleep行不行呢?
使用join,则必须要t1彻底执行完t2才能运行。如果是希望t1先干50%的活就让t2开始行动,join无能为力;使用sleep是指定一个休眠时间,但是t1执行的这些活到底花了多少时间不好估计~

使用wait和notify可以更好地解决上述的问题。
(这么看来wait和notify的功能不是包含了join的功能吗?那么为什么还要专门搞一个join呢?wait notify 使用起来要比 join 麻烦不少,join相当于是某些情况下专用的~~)

完成这个协调工作, 主要涉及到三个方法:

  • wait() / wait(long timeout):让当前线程进入等待状态;
  • notify() / notifyAll():唤醒在当前对象上等待的线程。

注意:wait、notify、notifyAll 都是 Object 类的方法!

3.2 wait()方法

某个线程调用wait()方法,就会进入阻塞 (无论是通过哪个对象wait的),此时就处在WAITING状态!

wait 做的事情:

  • 释放当前的锁
  • 使当前执行代码的线程进行阻塞等待
  • 满足一定条件时被唤醒,重新尝试获取这个锁,获取锁后继续向下执行

wait 要搭配 synchronized 来使用;脱离 synchronized 使用 wait 会直接抛出异常!

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本,来指定等待时间)
  • 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常

注意:
1)很多带有阻塞功能的方法都带 InterruptedException异常,这些方法都是可以被interrupt方法通过这个异常给唤醒的!
2)无参版本如果没有notify就会一直死等下去,这并不是一个好的选择~~因此很多情况下都是使用带参版本,指定等待的最大时间!

3.3 notify()方法

notify 方法是唤醒等待的线程。

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则由线程调度器随机挑选出一个呈 wait 状态的线程。(并没有”先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

3.4 示例

观察以下代码:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException{
        Object object = new Object();
        object.wait();
    }
}

运行结果分析:
在这里插入图片描述
为什么会出现这个异常?
上面提到:wait方法首先做的事是释放锁!没有锁哪来的释放呢?~
因此wait操作要搭配synchronized来使用!:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此中间可以sleep适当时间,完整代码:

class Counter{
    public volatile int flag = 0;
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException{
        Object object = new Object();

        Thread t1 = new Thread(() -> {
            System.out.println("t1: wait之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("t1: wait之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify之前");
            synchronized (object){
                object.notify();
            }
            System.out.println("t2: notify之后");
        });

        t1.start();
        Thread.sleep(500);
        t2.start();
    }
}

运行结果:
在这里插入图片描述

3.5 notifyAll()方法

notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程!

注意: 虽然是同时唤醒多个线程,但是这多个线程需要竞争锁,所以并不是同时执行,而仍然是有先有后的执行!

notifyAll和notify相似:
多个线程 wait 的时候,notify随机唤醒一个,notifyAll所有线程都唤醒,这些线程再一起竞争锁…
在这里插入图片描述
在这里插入图片描述

3.6 补充知识点

1)wait和sleep

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。唯一的相同点就是都可以让线程放弃执行一段时间!
在这里插入图片描述

其次:

  • wait 需要搭配 synchronized 使用;sleep 不需要
  • wait 是 Object 的方法;sleep 是 Thread 的静态方法

2)线程唤醒顺序

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/118585.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!