【JavaEE】_线程安全

目录

1. 线程不安全问题

2. 线程不安全的原因

3. 解决线程不安全问题


1. 线程不安全问题

线程安全问题是多线程编程必须考虑的重要问题,也因为其难以理解与处理,故而程序员也尝试发明更多的编程模型来处理并发编程,如多进程、多线程、actor、csp等等;

我们知道,操作系统调度线程是抢占式执行,这样的随机性可能会导致程序执行出现一些bug,如果由于这样的调度的随机性使得代码出现了bug,则认为代码是不安全的,如果没有引入bug,则认为代码是安全的;

线程不安全的典型案例:使用两个线程对同一个整型变量进行自增操作,每个线程自增五万次:

class Counter{
    //保存两个线程要自增的变量
    public int count = 0;
    public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        t1.start();
        t2.start();
        //在main线程中打印两个线程自增结束后得到的count结果
        //t1、t2执行结束后再打印count结果
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行三次,输出结果与预期并不相符且多次运行多次不同:

          

注:1.t1.join()与t2.join()谁先谁后均可:

线程是随机调度的,t1、t2线程的结束前后是未知的,

如果t1先结束,则先令main线程等待t1结束,待t1结束后再令main线程等待t2结束;

如果t2先结束,仍先令main等待t1结束,t2结束了t1还未结束,main线程仍然在等待t1结束,等t1结束后,t2已经结束了,则此时t2.jion()立即返回;

2.站在CPU角度来看,count++实际上是3个CPU指令:

第一步:将内存中count值加载到CPU寄存器中;(load)

第二步:寄存器中的值将其+1;(add)

第三步:把寄存器中的值写回到内存的count中;(save)

由于抢占式执行,两个线程同时执行这三个指令的时候顺序上充满了随机性,只有当两个线程的三条指令串型执行的时候才会符合预期,只要三条指令出现交错,就会出现错误,如:

2. 线程不安全的原因

(1)根本原因:线程是抢占式执行,线程间的调度充满随机性;

(2)修改共享数据:多个线程对同一个变量进行修改操作,才会导致线程不安全问题;

当多个线程分别对不同的多个变量进行操作,或是多个线程对同一个变量进行读操作,都不会导致线程不安全问题;

(3)操作的原子性问题:针对变量的操作不是原子性的,就会导致线程不安全问题,如上文示例中,自增操作其实是3条指令;

当操作是原子性的,如读取变量的值就只对应一条机器指令,就不会导致线程不安全问题;

(4)内存可见性问题:java编译器的优化操作使得在某些情况下线程之间出现信息不同步问题:

如线程t1一直在高速循环进行读操作,线程t2不定时进行修改操作,此时由于t1的高速访问可能无果,就会停止将数据从内存中读至寄存器中再进行读取,而直接从寄存器中读取,此时若t2线程进行修改操作,就会由于内存可见性问题而使两个线程信息不同步,出现安全问题,示例代码如下:

import java.util.Scanner;
public class Demo2 {
    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while(0 == isQuit){

           }
            System.out.println("Thread t has finished.");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input the value of isQuit: ");
        isQuit = scanner.nextInt();
        System.out.println("Thread main has finished.");
    }
}

输出结果为:

并未输出"Thread t has finished."说明t线程并未结束; 

(5)指令重排序问题:指令重排序也是编译器优化的一种操作,编译器在某些情况下可能调整代码的先后顺序来提高程序的效率,单线程通常不会出现问题,但在多线程代码中,可能就会误判导致线程安全问题;

3. 解决线程不安全问题

对应上文的线程不安全问题原因,思考解决线程不安全问题的方法:

(1)线程调度的随机性问题:无法从代码层面进行改进的;

(2)多线程修改同一变量问题:部分情况下可调整代码结构,使不同线程操作不同变量;

(3)变量操作的原子性问题:加锁操作将多个操作打包为一个原子性操作;

(4)内存可见性问题:

① 使用synchronized关键字可以保证内存可见性,被synchronied修饰的代码块,相当于手动禁止了编译器的优化;

② 使用volatile关键字可以保证内存可见性,禁止编译器做出上述优化:

import java.util.Scanner;
public class Demo2 {
    private static volatile int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while(0 == isQuit){

           }
            System.out.println("Thread t has finished.");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input the value of isQuit: ");
        isQuit = scanner.nextInt();
        System.out.println("Thread main has finished.");
    }
}

此时输出结果为:

  

(5)指令重排序问题:synchronized关键字可以禁止指令重排序;

注:synchronized解决多线程修改同一变量问题代码示例:

使用锁后,就将线程间乱序的并发变成了一个串型操作,并发性降低但会更安全;

虽然效率有所降低但相较于单线程程序,还是能分担步骤压力,效率还是较高的;

java中加锁的方式有很多种,最常使用的是synchronized关键字:

class Counter{
    public int count=0;
    synchronized public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

输出结果为:

  

注:(1)在increase()方法前加上synchronized修饰,此时进入方法就会自动加锁,离开方法就会自动解锁;

(2)当给一个线程加锁成功时,其他线程尝试加锁就会触发阻塞等待,此时对应的线程就处于clocked状态;

(3)阻塞状态会一直持续到占用锁的线程解锁为止,时间轴缩略图如下:

 (3)synchronized可以保证操作的原子性,保证内存可见性,还可以禁止指令重排序;

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

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

相关文章

m4v如何转换成mp4?这4个转换方法总结好了

M4V是一种常见的视频格式&#xff0c;特别是在苹果设备上。然而&#xff0c;有时候我们可能需要将M4V文件转换成MP4格式&#xff0c;以便在不同的设备和软件上播放。m4v如何转换成mp4&#xff1f;本文为你总结了4个简单易行的转换方法&#xff0c;让你轻松实现格式转换。无论你…

FPGA 高速接口(LVDS)

差分信号环路测试 1 概述 LVDS&#xff08;Low Voltage Differential Signalin&#xff09;是一种低振幅差分信号技术。它使用幅度非常低的信号&#xff08;约350mV&#xff09;通过一对差分PCB走线或平衡电缆传输数据。大部分高速数据传输中&#xff0c;都会用到LVDS传输。 …

Python自动化测试之异常处理机制知识讲解

一、前言 今天笔者还是想要讲python中的基础&#xff0c;主要讲解Python中异常介绍、捕获、处理相关知识点内容&#xff0c;只有学好了这些才能为后续自动化测试框架搭建及日常维护做铺垫&#xff0c;废话不多说我们直接进入主题吧。 二、异常处理合集 2.1 异常处理讲解 在…

数据库||数据库的安全性

1.实验题目&#xff1a;数据库的安全性 2.实验目的和要求&#xff1a; 掌握SQL Server 2008的安全控制机制掌握SQL Server2008的身份验证模式理解数据库用户帐户的基本概念理解角色的概念 3.实验步骤&#xff1a; 按实验内容要求完成各项操作根据题目要求给出解决方案提交实…

如何找回丢失照片? 7 种免费照片恢复方法分享

照片可以勾起回忆&#xff0c;让我们想起与最亲近的人一起度过的时光&#xff0c;这就是为什么仅仅丢失一张重要照片就会让人感觉完全毁灭性的——几乎就像你失去了记忆本身一样。好消息是&#xff0c;大多数丢失或意外删除的照片都可以使用照片恢复软件恢复&#xff0c;而且我…

appium实现自动化测试原理

目录 1、Appium原理 1.1、Android Appium原理图文解析 1.1.2、原理详解 1.1.2.1、脚本端 1.1.2.2、appium-server 1.1.2.3、中间件bootstrap.jar 1.1.2.4、驱动引擎uiautomator 1.2、 IOS Appium原理 1、Appium原理 1.1、Android Appium原理图文解析 执行测试脚本全过…

IDEA-常用插件

1、Mybatis Log Free 当我们使用mybatis log在控制台输出sql 内容&#xff0c;输出内容将语句与参数分开打印&#xff0c;还需要手动将参数替换到指定位置。 使用对应插件后&#xff0c;自动将输出内容组装成完整的可直接执行的SQL 在插件市场 查看对应名称&#xff0c;并安装。…

Postman路径修改

默认安装好Postman之后&#xff0c;默认路径在&#xff1a;C:\Users\用户名\AppData\Local\Postman。 修改路径只需要将整个文件夹拷贝到需要移动的位置即可&#xff0c;然后重新创建一个快捷方式。再删除原来路径的文件夹。

【Java EE初阶十八】网络原理(三)

3. 网络层 网络层要做的事情主要是两方面&#xff1a; 1)、地址管理&#xff1a;制定一系列的规则,通过地址,描述出网络上一个设备的位置&#xff1b; 2)、路由选择&#xff1a;网络环境是比较复杂的&#xff0c;从一个节点到另一个节点之间&#xff0c;存在很…

2024阅读AOSP源码的正确打开姿势

2024阅读AOSP源码的正确打开姿势 在Android系统开发中&#xff0c;阅读AOSP&#xff08;Android Open Source Project&#xff09;源码是非常重要的环节。然而&#xff0c;如何构建一个优雅的阅读环境却是一个不容忽视的问题。本文将介绍不同工具和方法下阅读AOSP源码的正确姿…

渲染效果图为什么都选择使用云渲染,优势有哪些?

随着3D技术在动画、电影、游戏、建筑等领域的广泛应用&#xff0c;渲染作为3D制作流程中的重要环节&#xff0c;其效率和成本直接影响着项目的最终成果。 近年来&#xff0c;云渲染技术的兴起为3D创作者提供了一种全新的选择&#xff0c;与传统的本地渲染相比&#xff0c;云渲…

【Wio Terminal】输入/输出

输入/输出 一、概述1、硬件原理图Terminal引脚分布及功能Wio Terminal Grove端口引脚分配 二、使用Wio Terminal上的Grove模拟端口1、RPI 模拟引脚2、Grove引脚配置3、示例 三、使用 Wio Terminal上的Grove数字端口1、RPI 数字引脚2、Grove引脚配置将 Grove I2C 端口用作数字端…

数据结构1.0(基础)

近java的介绍&#xff0c; 文章目录 第一章、数据结构1、数据结构 &#xff1f;2、常用的数据结构数据结构&#xff1f; 逻辑结构and物理结构 第二章、数据结构基本介绍2.1、数组&#xff08;Array&#xff09;2.2、堆栈&#xff08;Stack&#xff09;2.3、队列&#xff08;Que…

2024阿里云云服务器ECS价格表出炉

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

C++之Easyx——图形库的基本准备工作

什么是Easyx&#xff1f; EasyX Graphics Library 是针对 Visual C 的免费绘图库&#xff0c;支持 VC6.0 ~ VC2022&#xff0c;简单易用&#xff0c;学习成本极低&#xff0c;应用领域广泛。目前已有许多大学将 EasyX 应用在教学当中。 它比Red PandaDev C上的图形库功能要强…

海外大带宽服务器连接失败:原因与解决策略

​随着全球互联网的发展&#xff0c;越来越多的企业和个人选择使用海外大带宽服务器来满足数据传输和业务需求。然而&#xff0c;在实际使用中&#xff0c;有时会出现服务器连接失败的问题。本文将为您分析原因并提供相应的解决策略。 一、海外大带宽服务器连接失败的原因 网络…

Feign 第一次调用为什么会很慢?

前言 首先要了解 Feign 是如何进行远程调用的&#xff0c;这里面包括&#xff0c;注册中心、负载均衡、FeignClient 之间的关系&#xff0c;微服务通过不论是 eureka、nacos 也好注册到服务端&#xff0c;Feign 是靠 Ribbon 做负载的&#xff0c;而 Ribbon 需要拿到注册中心的…

libgdx的完整教程

概述&#xff1a;如果大家想用java做游戏&#xff0c;那么libgdx可能是一个好的选择。LibGDX是一个强大的跨平台2D/3D游戏开发框架。 LibGDX的主要特点包括&#xff1a; 强兼容性&#xff1a;LibGDX能够运行在多种平台上&#xff0c;包括但不限于Mac、Linux、Windows以及Andr…

零基础学编程从哪开始,中文编程工具免费版下载及构件用法教程

一、前言 零基础学编程从哪开始&#xff0c;中文编程工具免费版下载及构件用法教程 今天给大家分享的中文编程开发语言工具资料如下&#xff1a; 编程入门视频教程链接 http://​ https://edu.csdn.net/course/detail/39036 ​ 编程工具及实例源码文件下载可以点击最下方…

酷开会员丨春节回家,用酷开系统欢聚K歌,唱出团圆喜乐

每当春节临近&#xff0c;总会在大街小巷听到熟悉的旋律&#xff0c;比如《恭喜发财》、比如《好运来》……这些氛围感满满的歌曲&#xff0c;代表着年味&#xff0c;寓示着团圆。 春节&#xff0c;是万家团圆的喜庆时刻&#xff0c;也是亲朋好友欢聚一堂的日子。亲友聚会&…