【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

这篇具有很好参考价值的文章主要介绍了【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

摄影分享~~
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

volatile关键字

volatile能保证内存可见性

import java.util.Scanner;

class MyCounter {
    public int flag = 0;
}

public class ThreadDemo14 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();

        Thread t1 = new Thread(() -> {
            while (myCounter.flag == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 循环结束");
        });

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

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

以上代码运行的结果可能是输入1后,t1这个线程并没有结束。而是一直在while中循环。而t2线程已经执行完了。
以上情况,就叫做内存可见性问题
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
这里使用汇编来理解,大概分为两步操作:

  1. load,把内存中flag的值,读到寄存器中。
  2. cmp,把寄存器中的值,和0进行比较。根据比较结果,决定下一步往哪个地方执行(条件跳转指令)

上述循环循环体为空,循环执行速度极快。循环执行很多次,在t2真正修改之前,load得到的结果都是一样的。另一方面,load操作和cmp操作相比,速度慢的多得多。由于load执行速度太慢(相比于cmp),再加上反复load到的结果都是一样的,JVM就做出了一个大胆的决定:不再真正的重复load,判定没有人修改flag值(但实际上是有人在修改的,t2在修改),直接就读取一次就好。(编译器优化的一种方式)
内存可见性问题:一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改。此时读到的值,并不一定是修改之后的值。(jvm/编译器在多线程环境下优化时残生了误判)
此时,我们就需要手动干预了。我们可以给flag这个变量加上volatile关键字。告诉编译器,这个变量是“易变”的,需要每一次都重新读取这个变量的内容。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
volatile不保证原子性,原子性是由synchronized来保证的。

wait和notify

举个列子:
t1,t2两个线程,希望t1先执行任务,任务执行快结束了让t2来干,就可以让t2先wait(阻塞,主动放弃cpu)。等t1任务执行快结束了,在通过notify通知t2,把t2唤醒,让t2开始执行任务。
上述场景中,使用join和sleep可以吗?
使用join,必须要t1彻底执行完,t2才能执行。如果希望t1执行一半任务然后让t2执行,join无法完成。
使用sleep,必须制定一个休眠时间,但是t1执行任务的时间是难以估计的。
使用wait和notify可以解决上述问题。

wait

wait进行阻塞,某个线程调用wait方法,就会进入阻塞,此时就处于WAITING.
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
这个异常,很多带有阻塞功能的方法都带,这些方法都是可以被interrupt方法通过以上异常唤醒。
我们再来看一个代码:

public class ThreadDemo17 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行完毕!");
            });

            t.start();
            System.out.println("wait前");
            t.wait();
            System.out.println("wait后");
        }
}

【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
这里会出现非法的锁状态异常。锁的状态一般是被加锁的状态,被解锁的状态。
为什么会出现这个异常呢?和wait的操作有关:

wait的操作:

  1. 先释放锁
  2. 进行阻塞等待
  3. 收到通知后,重新尝试获取锁,并且在获取锁后,继续往下执行。

上述代码没有锁就想要释放锁,所以出现了非法的锁状态异常。
因此,wait操作要搭配synchronized来使用。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

notify

wait和notify一般搭配使用。notify方法用来唤醒wait等待的线程, wait能够释放锁, 使线程等待, 而notify唤醒线程后能够获取锁, 然后使线程继续执行。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
如果上述代码中,t1还没有执行wait,t2已经执行了notify,那么此时的声明就是没有用的。t2执行notify后,t1执行wait后会一直阻塞等待。
注意上述代码在t2唤醒t1之后,t1和t2之间的执行是随机的,也是就标号3和标号4的地方的顺序是不确定的。

方法 效果
wait(); 无参数,一直等直到notify唤醒
wait(时间参数); 指定最长等待时间

notifyAll

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
一般情况下,使用notify。因为全部唤醒会导致线程之间抢占式执行。不一定安全。

wait和sleep的区别

相同点:

  • 都可以使线程暂停一段时间来控制线程之间的执行顺序.
  • wait可以设置一个最长等待时间, 和sleep一样都可以提前唤醒.

不同点:

  • wait是Object类中的一个方法, sleep是Thread类中的一个方法.
  • wait必须在synchronized修饰的代码块或方法中使用, sleep方法可以在任何位置使用.
  • wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAITING状态,不涉及锁相关的操作.
  • 使用sleep只能指定一个固定的休眠时间, 线程中执行操作的执行时间是无法确定的; 而使用wait在指定操作位置就可以唤醒线程.
  • sleep和wait都可以被提前唤醒, interruppt唤醒sleep, 是会报异常的, 这种方式是一个非正常的执行逻辑; 而noitify唤醒wait是正常的业务执行逻辑, 不会有任何异常.

小练习

有三个线程, 分别只能打印 A, B, C. 控制三个线程固定按照 ABC 的顺序来打印.

public class ThreadDemo18 {
    // 有三个线程, 分别只能打印 A, B, C. 控制三个线程固定按照 ABC 的顺序来打印.
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (locker1) {
                locker1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1) {
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            System.out.println("B");
            synchronized (locker2) {
                    locker2.notify();
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker2) {
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.start();
    }
}

创建locker1,供1,2使用
创建locker2,供2,3使用
线程3,locker2.wait()
线程2, locker1.wait()唤醒后执行locker2.notify
线程1执行自己的任务,执行完后locker.notify
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

多线程案例

单例模式

单例模式是设计模式的一种。
单例模式能保证某个类在程序中只存在唯一一份的实例,而不会创建出多个实例。
单例模式具体的实现方式分为“饿汉”和“懒汉”。

饿汉模式

类加载的同时,创建实例。
类对象在一个java进程中,只有一份。因此类对象内部的类属性也是唯一的。
在类加载阶段,就把实例创建出来了。

//饿汉模式的单例模式的实现
//保证Singleton这个类只能创建出一个实例
class Singleton{
    //在此处,先将实例创建出来
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
    //为了避免Singleton类不小心被多复制出来
    //把构造方法设为private,在类外,无法通过new的方式来创建一个Singleton
    private Singleton(){

    }
}
public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //Singleton s3 = new Singleton();
        System.out.println(s == s2);
    }
}

【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

  • static保证这个实例唯一
  • static保证这个实例被创建出来。

懒汉模式

class SingletonLazy{
    private static SingletonLazy instance = null;


    public static SingletonLazy getIsntance() {
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}
public class ThreadDemo20 {
        public static void main(String[] args) {
            SingletonLazy s = SingletonLazy.getIsntance();
            SingletonLazy s2 = SingletonLazy.getIsntance();
            System.out.println(s == s2);
        }
}

【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

在多线程中调用instance,饿汉模式是线程不安全的。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
那么如何保证懒汉模式线程安全呢?
**加锁。**线程安全的本质问题,就是读,比较,写这三个操作不是原子的。所以我们可以加锁来解决线程安全问题。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式

但是,加锁操作就导致每次调用getInstance都需要花一定的开销。而我们的加锁只针对new对象之前,所以我们就可以判断一下对象是否创建,再去决定加锁。
如果对象创建了,就不加锁。如果对象没有创建,就加锁。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
上述代码还存在一个问题,即内存可见性问题:
假如调用getInstance的线程有很多,此时代码就有可能被优化(第一次读内存,后续读的是寄存器/cache)
除此之外,可能还会涉及到指令重排序。
【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
上述代码中,分为三个步骤:

  1. 申请内存空间
  2. 调用构造方法,把这个内存空间初始化成一个对象
  3. 把内存空间的地址赋值给instance引用

而编译器的指令重排序操作就会调整代码执行顺序,123可能会变成132.(单线程中没有影响)
我们可以给代码中加上volatile。
volatile有两个功能:

  1. 解决内存可见性
  2. 禁止指令重排序。

以下为懒汉模式的单例模式的完整代码:文章来源地址https://www.toymoban.com/news/detail-425019.html

class SingletonLazy{
    private volatile static SingletonLazy instance = null;


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

        return instance;
    }
}
public class ThreadDemo20 {
        public static void main(String[] args) {
            SingletonLazy s = SingletonLazy.getInstance();
            SingletonLazy s2 = SingletonLazy.getInstance();
            System.out.println(s == s2);
        }
}

到了这里,关于【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • volatile 关键字详解

    目录 volatile volatile 关键用在什么场景下: volatile 防止编译器优化: volatile   是一个在许多编程语言中(包括C和C++)用作的标识符。它用于告诉编译器不要对带有该修饰的变量进行优化,以确保变量在特定情况下的可见性和预测性。 在C和C++中, volatile

    2024年02月11日
    浏览(43)
  • 【C】volatile 关键字

    1)基本概念 const 是C语言的一个。 const 用于告诉编译器相应的变量可能会在程序的控制之外被修改,因此编译器不应该对其进行优化。 声明语法: 作用: 防止编译器优化,确保对变量的每次访问都是实际的读写操作,而不是使用缓存中的值。 用于表示可能会 被异步

    2024年01月22日
    浏览(42)
  • JAVA volatile 关键字

    volatile 是JAVA虚拟机提供的轻量级的同步机制,有三大特性 1、保证可见性  2、不保证原子性  3、禁止指令重排 JMM  JAVA内存模型本身是一种抽象的概念并不真实存在 它描述的是一组规则或规范,提供这组规范定义了程序中各个变量(包括实例变量、静态变量)的访问方式。

    2024年02月13日
    浏览(52)
  • 【Java基础】volatile关键字

    关于作者:CSDN内容合伙人、技术专家, 从零开始做过日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习Java基础知识,温故知新。 volatile 是一个Java,可以用来修饰变量,volatile也被称为轻

    2024年02月11日
    浏览(45)
  • C语言volatile关键字

    在C语言中, volatile 是一个类型修饰符,用于告诉编译器对象的值可能会在编译器无法检测到的情况下被改变。这通常发生在以下两种情况: 硬件的输入/输出操作,例如一个设备寄存器的读取或写入。 共享内存的并行程序,其中一个线程修改了一个内存位置,而另一个线程

    2024年02月07日
    浏览(54)
  • 浅析Java中volatile关键字

            Java中的volatile用于修饰一个变量,当这个变量被多个线程共享时,这个变量的值如果发生更新,每个线程都能获取到最新的值。volatile在多线程环境下还会禁止指令重排序,确保变量的赋值操作按照代码的顺序执行。需要注意是它不能保证变量操作的

    2024年01月21日
    浏览(48)
  • volatile关键字(轻量级锁)

    目录 一、volatile出现背景 二、JMM概述 2.1、JMM的规定  三、volatile的特性 3.1、可见性  3.1.1、举例说明  3.1.2、总结 3.2、无法保证原子性 3.2.1、举例说明 3.2.2、分析 3.2.3、使用volatile对原子性测试  3.2.4、使用锁机制  3.2.5、总结 3.3、禁止指令重排序  四、volatile的内存语义 4

    2024年02月15日
    浏览(40)
  • Java中的volatile关键字实现原理

    在并发编程中,线程之间的可见性问题是非常重要的一项难题。Java中提供了一种解决并发可见性问题的机制,即volatile。 在本文中,我们将会讲解Java中volatile的实现原理,为什么它能够保证可见性,以及背后的实现原理涉及到的内存屏障和JVM屏障等内容。在学习

    2023年04月27日
    浏览(42)
  • C/C++ 中 volatile 关键字详解

    转载自菜鸟教程【C/C++ 中 volatile 详解 | 菜鸟教程】 1、为什么用volatile? C/C++ 中的 volatile 和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 \\\"The C++ Programming Language\\\" 对 volatile 修饰词的说明: A volatile specifier is a hint to a compiler that an ob

    2024年02月11日
    浏览(52)
  • Java面试题:请谈谈Java中的volatile关键字?

    在Java中,volatile是一种特殊的修饰符,用于确保多线程环境下的变量 可见性和顺序性 。当一个变量被声明为volatile时,它可以确保以下两点: 内存可见性 :当一个线程修改了一个volatile变量的值,其他线程会立即看到这个改变。这是因为volatile会禁止CPU缓存和编

    2024年04月23日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包