[Java EE] 多线程(四):线程安全问题(下)

1.5 volatile关键字

我们在了解这个关键字之前,我们首先要把产生线程安全的第4个原因补齐,我们来说说由于内存可见性引起的线程安全问题.
我们来看下面这样一段代码:

import java.util.Scanner;

public class Demo16 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (count == 0) {
                ;
            }
            System.out.println("count的值被修改");
        });
        Thread thread1 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            count = scanner.nextInt();
        });
        thread.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

由上述代码的逻辑,我们预期的结果是:在控制台的地方输入了一个不为0的数字的时候,上面的循环条件不满足,跳出循环,打印"count的值被修改",但是我们运行起来发现:
在这里插入图片描述
输入了不为0的值之后,毛都没有!!
为什么会出现这种情况呢?我们首先要了解一下Java中的内存模型(JMM),即Java虚拟机规范中定义的Java内存模型.
在这里插入图片描述
设计这种内存模型的目的就是为了保证:屏蔽掉各种硬件和操作系统的访问差异,避免由于操作系统不同和硬件结构的不同而引起的不兼容,真正做到"一次编译,到处运行".

  • 线程之间的共享变量存在主内存(Main Memory).(硬件角度总真正的内存空间)
  • 每⼀个线程都有自己的"⼯作内存"(Working Memory).(硬件角度的CPU中的寄存器)
  • 线程想要读取一个共享变量的时候,需要先从主内存中拷贝到工作内存中.
  • 线程想要修改一个共享变量的时候,先修改工作内存中的副本,再同步回主内存中.

但是当计算机发现每次从主内存中拷贝数据到工作内存中时,每次拷贝的数据都是一样的,而且工作内存的访问要比主内存的访问快好几个数量级,这就使得计算机不得不做出优化操作,每次读取变量的时候,把拷贝这一步优化掉了,直接读取的是工作内存中的数据,这就使得在另一个线程在主内存中修改这个共享变量的时候,另一个线程读取不到,也就无法改变工作内存中的数据.
但是我们在循环中加上一个print操作的时候,我们会惊奇地发现,循环可以停下来了:

import java.util.Scanner;
public class Demo17 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (count == 0) {
                System.out.println("tread");//IO输出比count判断慢得多
            }
            System.out.println("count的值被修改");
        });
        Thread thread1 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            count = scanner.nextInt();
        });
        thread.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

在这里插入图片描述
这又是为什么呢?是因为由于拷贝操作时内存方面的操作,速度肯定又要比IO操作快好几个数量级.而IO操作每次执行的结果注定是不同的,这就使得JVM不可以优化掉IO操作,即然IO操作都优化不到,比他快好几个数量级的拷贝操作肯定优化不到.

举例说明:年会不能停
有请助教:潘妮,马杰克,胡建林,杰弗瑞.
假如现在没有胡建林,现在潘妮就相当于主内存与工作内存中的拷贝操作,马杰克就相当于直接从工作内存中读取的操作.如今杰弗瑞要裁员,由于马杰克的工作效率和工作能力比潘妮大上好几个数量级,裁员的时候优先裁的就是潘妮,但是现在歪打正着混进个胡建林,就相当于IO操作,由于胡建林是靠着"关系"进入的众和集团,即使胡建林干活再慢,也不可以裁掉,即然比潘妮干活慢的胡建林都裁不掉,何谈裁潘妮.
在这里插入图片描述

现在如果没有IO操作,还想要while循环停下来,我们就可以引入volatile关键字来对变量进行修饰.告诉JVM,该变量必须每次都到主内存中读取,不可以进行优化操作,就相当于告诉老板,裁员的时候请不要裁到大动脉!!!

import java.util.Scanner;

public class Demo18 {
    public static volatile int count = 0;//使用volatile告诉编译器该变量不可优化
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (count == 0) {
                ;
            }
            System.out.println("count的值被修改");
        });
        Thread thread1 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            count = scanner.nextInt();
        });
        thread.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

在这里插入图片描述
但是我们需要注意的是:volatile不保证原子性

public class Demo21 {
    public static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count);
    }
}

在这里插入图片描述
我们看到,即使给count变量加了volatile关键字,还是会存在线程安全问题.

1.6 wait与notify–>线程的等待通知机制

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

举例说明:光与影
有请助教:黑子哲也,青峰大辉,火神大我
在与漂亮国的对决中,他们像拿到一个二分球,就先需要大辉先抢球,之后传球给黑子,黑子通过回旋一击,使得篮球改变运动轨迹,之后通过大我的扣篮完成完美一击.
他们中间的先后顺序,但凡慢了一步,或者是快了一步,都会丢失这2分.
在这里插入图片描述
在这里插入图片描述

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

  • wait()/wait(time):让线程进入等待状态
  • notify()/notifyAll():唤醒在当前对象上等待的所有线程
    注意:这几个方法都是Object类的方法

1.6.1 wait()方法

wait做的事情:

  • 释放当前对象的锁
  • 使得当前线程进入阻塞等待状态
  • 满足一定条件被唤醒之后,尝试重新获取锁
    注意:wait要搭配synchronized使用,否者在wait()方法解锁的时候,找不到对应的锁,就会抛出异常.
    wait结束等待的条件:
  • 其他线程使用调用notify方法唤醒该线程.
  • wait时间超时.
  • 其他线程调用该线程的interrupted方法,导致线程被强制唤醒,抛出异常.

1.6.2 notify()方法

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

public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread thread = new Thread(()->{
            synchronized (object){
                System.out.println("等待开始");
                try {
                    object.wait();//1.解锁,此时tread1才可以加锁 2.阻塞等待 3.唤醒之后再尝试获取锁 4.唤醒之后tread1未解除锁,阻塞
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束");//所以等待结束的打印一定在唤醒结束之后
            }
        });
        Thread thread1 = new Thread(()->{
            synchronized (object){
                System.out.println("唤醒开始");
                object.notify();
                System.out.println("唤醒结束");
            }
        });
        thread.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

注意:在tread启动之后,必须sleep1s,由于线程调度的随机性,不加的话很可能先调度tread1现成,这样notify就会被wait错过

1.6.3 notifyAll()方法

上述notify方法只能随机唤醒一个线程:

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread thread = new Thread(()->{
            synchronized (object){
                System.out.println("等待开始");
                try {
                    object.wait();//1.解锁,此时tread1才可以加锁 2.阻塞等待 3.唤醒之后再尝试获取锁 4.唤醒之后tread1未解除锁,阻塞
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束");//所以等待结束的打印一定在唤醒结束之后
            }
        });
        Thread thread2 = new Thread(()->{
            synchronized (object){
                System.out.println("等待开始1");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束1");
            }
        });
        Thread thread1 = new Thread(()->{
            synchronized (object){
                System.out.println("唤醒开始");
                object.notify();
                System.out.println("唤醒结束");
            }
        });
        thread.start();
        thread2.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

运行结果:只唤醒了tread线程
在这里插入图片描述
如果想要一次性唤醒所有线程,就要用到notifyAll()方法:

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread thread = new Thread(()->{
            synchronized (object){
                System.out.println("等待开始");
                try {
                    object.wait();//1.解锁,此时tread1才可以加锁 2.阻塞等待 3.唤醒之后再尝试获取锁 4.唤醒之后tread1未解除锁,阻塞
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束");//所以等待结束的打印一定在唤醒结束之后
            }
        });
        Thread thread2 = new Thread(()->{
            synchronized (object){
                System.out.println("等待开始1");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束1");
            }
        });
        Thread thread1 = new Thread(()->{
            synchronized (object){
                System.out.println("唤醒开始");
                object.notifyAll();
                System.out.println("唤醒结束");
            }
        });
        thread.start();
        thread2.start();
        Thread.sleep(1000);
        thread1.start();
    }
}

运行结果:线程全部被唤醒
在这里插入图片描述
在两个线程全部被唤醒之后,由于唤醒之后线程会尝试重新获取锁,tread和tread2还是会发生锁竞争.

举例:面试
有请助教:滑稽老铁
notify()方法:
在这里插入图片描述
notifyAll()方法:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/579470.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PotatoPie 4.0 实验教程(25) —— FPGA实现摄像头图像直方图均衡变换

图像的直方图均衡是什么&#xff1f; 图像的直方图均衡是一种用于增强图像对比度的图像处理技术。在直方图均衡中&#xff0c;图像的像素值被重新分配&#xff0c;以使得图像的直方图变得更均匀&#xff0c;即各个像素值的分布更加平衡。这意味着直方图中每个像素值的频率大致…

在PR中使用 obs 和 vokoscreen 录制的视频遇到的问题

1. obs 录制的视频 在 Adobe Premiere Pro CS6 中只有音频没有视频 2. vokoscreen 录制的视频&#xff0c;没有声音 这是是和视频录制的编码有关系&#xff0c;也和显卡驱动关系 首先 obs 点击 文件 ---> 设置 录制的视频都是可以正常播放的&#xff0c;在PR不行。更…

python爬虫 - 爬取 json 格式数据(股票行情信息:雪球网,自选股)

文章目录 1. 第一步&#xff1a;安装requests库2. 第二步&#xff1a;获取爬虫所需的header和cookie3. 第三步&#xff1a;获取网页4. 第四步&#xff1a;解析网页5. 第五步&#xff1a;解析 json 结构数据体6. 代码实例以及结果展示 python爬虫五部曲&#xff1a; 第一步&…

字符串变量 字符串常量

仅个人笔记 #include<iostream> using namespace std;int main() {char str[] "2232344434";for (int i 0; i < strlen(str); i){printf("%c", *(stri));}const char* arr "12343545";for (int i 0; i < strlen(arr); i){printf…

HackMyVM-Vulny

目录 信息收集 arp nmap nikto WEB信息收集 主页信息收集 gobuster RCE漏洞 反弹shell 提权 系统信息收集 横向渗透 flock提权 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC…

mysql-sql-练习题-2-窗口函数

窗口函数 访问量max sum建表窗口函数连接 直播间人数 第1、3名建表排名sum 访问量max sum 每个用户截止到每月为止&#xff0c;最大单月访问次数&#xff0c;累计到该月的总访问次数 建表 create table visit(uid1 varchar(5) comment 用户id,month1 varchar(10) comment 月…

【热门话题】Chrome 插件研发详解:从入门到实践

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 Chrome 插件研发详解&#xff1a;从入门到实践一、引言二、Chrome 插件基础概念…

Win32 API 光标隐藏定位和键盘读取等常用函数

Win32 API 光标隐藏定位和键盘读取等常用函数 一、Win32 API二、控制台程序指令modetitlepausecls 三、控制台屏幕上坐标的结构体COORD四、句柄获取函数GetStdHandle五、控制台光标操作1.控制台光标信息结构体CONSOLE_CURSOR_INFO2.得到光标信息函数GetConsoleCursorInfo3. 设置…

Amazon云计算AWS之[5]关系数据库服务RDS

文章目录 RDS的基本原理主从备份和下读写分离 RDS的使用 RDS的基本原理 Amazon RDS(Amazon Relational Database Service) 将MySQL数据库移植到集群中&#xff0c;在一定的范围内解决了关系数据库的可扩展性问题。 MySQL集群方式采用Share-Nothing架构。每台数据库服务器都是…

《架构风清扬-Java面试系列第25讲》聊聊ArrayBlockingQueue的特点及使用场景

ArrayBlockingQueue是BlockingQueue接口的一个实现类之一 这个属于基础性问题&#xff0c;老规矩&#xff0c;我们将从使用场景和代码示例来进行讲解 来&#xff0c;思考片刻&#xff0c;给出你的答案 1&#xff0c;使用场景 实现&#xff1a;基于数组实现的有界阻塞队列&…

TCP/IP协议族中的TCP(二):解析其关键特性与机制

⭐小白苦学IT的博客主页⭐ ⭐初学者必看&#xff1a;Linux操作系统入门⭐ ⭐代码仓库&#xff1a;Linux代码仓库⭐ ❤关注我一起讨论和学习Linux系统 滑动窗口 在前面我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样…

【Python】#5 基础文件IO详解

文章目录 一、文件概述二、文件操作1.文件的打开与关闭2. 文件的读写2.1 读取2.2 写入tips:CSV与JSON文件 一些文件操作小实验《清明》文本写入与读取《红楼梦》人物出现统计&#xff08;部分文本&#xff09; 一、文件概述 文件是数据的集合和抽象&#xff0c;类似&#xff0…

如何增强交友、婚恋平台、金融等平台的安全性

运营商二要素核验是一种数字身份验证方法&#xff0c;主要使用用户的手机号码和姓名作为核验要素。这两个要素被认为是最基本的用户身份信息&#xff0c;通过运营商的数据库来核实其真实性。 在实际操作中&#xff0c;用户需要提供手机号码和姓名进行验证。应用系统会调用接口…

全面了解俄罗斯的VK开户和Yandex投放及内容运营

俄罗斯的VKontakte&#xff08;简称VK&#xff09;和Yandex是两个重要的在线平台&#xff0c;对于希望在俄罗斯市场进行推广的企业来说&#xff0c;了解如何在这些平台上开户和投放广告以及内容运营是非常关键的。 俄罗斯vk广告如何开户&#xff1f; 通过上海上弦进行俄罗斯V…

手写一个RNN前向传播以及反向传播

前向传播 根据公式 st tanh (Uxt Wst-1 ba) ot softmax(Vst by ) m 3 词的个数 n 5 import numpy as np import tensorflow as tf # 单个cell 的前向传播过程 # 两个输入&#xff0c;x_t&#xff0c;s_prev,parameters def rnn_cell_forward(x_t,s_prev,parameter…

每日OJ题_DFS回溯剪枝⑧_力扣494. 目标和

目录 力扣494. 目标和 解析代码&#xff08;path设置成全局&#xff09; 解析代码&#xff08;path设置全局&#xff09; 力扣494. 目标和 494. 目标和 难度 中等 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 或 - &#xff0c;然后串联…

SpringBoot + Vue实现Github第三方登录

前言&#xff1a;毕业设计终于好了&#xff0c;希望能有空多写几篇 1. 获取Github账号的Client ID和Client secrets 首先点击这个链接进入Github的OAuth Apps页面&#xff0c;页面展示如下&#xff1a; 之后我们可以创建一个新的apps: 填写资料&#xff1a; 创建之后就可以获…

WebGIS面试题(第六期)-GeoServer

WebGIS面试题&#xff08;第六期&#xff09; 以下题目仅为部分题目&#xff0c;全部题目在公众号 {GISer世界} &#xff0c;答案仅供参考!!! 因为本人之前做过相关项目用到了GeoServer&#xff0c;因此在简历上写了熟悉GeoServer。所以在相关面试中都有问到&#xff0c;所以我…

【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http板块)

【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器&#xff08;Http板块&#xff09; 一、思路图二、Util板块1、Splite板块&#xff08;分词&#xff09;&#xff08;1&#xff09;代码&#xff08;2&#xff09;测试及测试结果i、第一种测试ii、第二种…

[论文阅读] 3D感知相关论文简单摘要

Adaptive Fusion of Single-View and Multi-View Depth for Autonomous Driving 提出了一个单、多视图融合深度估计系统&#xff0c;它自适应地集成了高置信度的单视图和多视图结果 动态选择两个分支之间的高置信度区域执行融合 提出了一个双分支网络&#xff0c;即一个以单…