【Java EE初阶三 】线程的状态与安全(下)

这篇具有很好参考价值的文章主要介绍了【Java EE初阶三 】线程的状态与安全(下)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

3. 线程安全

        线程安全某个代码,不管它是单个线程执行,还是多个线程执行,都不会产生bug,这个情况就成为“线程安全”。

        线程不安全某个代码,它单个线程执行,不会产生bug,但是多个线程执行,就会产生bug,这个情况就成为 “线程不安全”,或者 “存在线程安全问题”。     

        举个线程不安全例子,我们计算一个变量的自增次数,它循环了100000次,用两个线程去计算,各自计算循环50000次的次数。   

3.1 线程不安全样例

        根本原因:线程的随机调度,抢占式执行

        代码结构:不同线程修改同一数据

        直接原因:多线程操作不是原子的

        由于线程的随机调度,抢占式执行(不可避免),代码结构会促进该原因加剧不良后果

       1、代码一分析---->代码随机调度,抢占执行的例子

        代码如下:

public class ThreadDemo4 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 50000; i <= 100000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
 
        t1.join();
        t2.join();
 
        System.out.println("count: " + count);
    }
}

        按照我们的逻辑,从1自增到10_0000,肯定是自增了10_0000次,但是结果如下图所示:

      【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

        我们实际答案却不是10_0000,是53978次,其出现如上情况的最主原因就是多线程代码它们是并发执行的,且往代码深层次分析,java中的count++语句是由cpu的三个指令构成的:

1)首先load 从内存中读取数据到cpu的寄存器中;

(2)其次add 把寄存器中的值 + 1;

(3)最后save 把寄存器中的值写回到内存中;
        因为上面两个线程t1和t2是并发执行的,那则t1 和 t2 线程的执行顺序就是无序的,他们可能同时读取内存中的数据add,双方都自增完往寄存器+1(应该是+1后再+1),但是最后从寄存器中save到内存中时,却只读取了一个线程自增完后的数值,另外一个自增的过程被忽略了,一些具体的分析如下图所示;

【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

        线程并发执行的结果是无数的,并不是简单的排列组合就能穷举出来,因为并发的原因,可能 t1 线程它执行了两次,才执行一次 t2 线程,或者 t2 执行的次数更多,t1 线程只执行一次。等以上这些情况都是有可能出现的。

        故此t1 和 t2自增的时候,就可能从寄存器中拿的是同一个值,这两线程的其中一个自增后,没有来得及在内存中进行自加1,另一个线程自增完后就直接往内存中那这个值了,最后的结果肯定是不符合我们预期的。

        故此由上图所示,符合我们预期的效果就只有最前面的两个情况了,但是这种情况也就是多线程串行化执行,执行完 t1,再执行t2,代码如下所示:

public class ThreadDemo4 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
 
            for (int i = 50000; i <= 100000; i++) {
                count++;
            }
        });
        t1.start();
 
        t2.start();
        t2.join();
 
        System.out.println("count: " + count);
    }
}

        结果如下:

      【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

        但是如此操作的话,这个代码和多线程的运行就完全没有关系了;

       

        2、代码二分析---->内存可见性例子

        代码如下:

package thread;

import java.util.Scanner;

public class ThreadDemo22 {
    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 1) {
                System.out.println("这里是线程t1");
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("请输入flag的值");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

        执行预期是当我们输入不等于1的值,就打印 “t1线程结束”,但是当我们输入结果为5,最终执行结果却不是我们预期的效果,执行结果如下:

【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

        

        flag值是!=1,但是t1线程一直在循环运行,虽然t2线程是在按照我们的要求改变flag的值,为什么结果与预期是相悖的,如此就涉及到了jvm内部的优化了,和内存可见性相关;

        t1线程中的flag==1这一操作有两个核心指令:

(1)load,读取内存中的flag值到寄存器

(2)拿着寄存器中的值和1进行比较(条件跳转指令)

        这里load的每次操作取到的值都是一样的,而当我们执行scanner操作修改flag的值时,load这一指令,已经执行上百亿次了;且从内存中取数据这一操作是非常耗时的,远远比条件跳转指令花时间,这时由于load开销太大,jvm就会产生怀疑,怀疑这个load继续操作的必要性;从而给出优化,把从内存读数据load这一操作给优化掉了,这样一来,jvm就不会再从内存中拿数据,而是把load拿到的值放到寄存器中,从寄存器拿到数据,进行比较。这样可以大幅度的提高循环的执行速度。

        上面的例子,t2修改了内存,但是t1没看到内存变化,就称为内存可见性问题。而内存可见性问题,是高度依赖编译器优化的问题;

 3.2 线程不安全问题解决方法

3.2.1  t1 循环里加sleep

        代码如下:

public class ThreadDemo3 {
    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 1) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //循环题里,啥也不写
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("请输入flag的值");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

        结果如下:

【Java EE初阶三 】线程的状态与安全(下),JAVA EE 初阶,java,开发语言,java-ee

        方法详解:因为10秒中都可以让t1线程里面的循环执行上百亿次(cpu飞快的从内存中网寄存器中读取数据),这样会导致load的开销就非常大,代码的优化迫切程度就比较大;但是加了sleep后,我们让线程t1进行休眠,如此load的开销就小了很多,代码的优化迫切程度就降低了,故此load就能将5这个值在优化前读取到寄存器和flag进行比较,最终达到我们预期的效果;

3.3.2 给flag变量加volatile修饰 

        代码如下:

public class ThreadDemo3 {
    private volatile static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 1) {
                //循环题里,啥也不写
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("请输入flag的值");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

        结果与之前类似,略;

        方法分析:

1、java提供volatile关键字,其核心作用,就是保证 “内存可见性”另一功能:禁止指令重排序)可以使jvm强迫的优化强制关闭,这样就可以确保循环每次都是从内存中拿数据了,虽然这样执行效率也会下降,但数据更为准确了;

2、我们对编译器优化是这样表述的:

        编译器发现,每次循环都要从内存中读取数据,内存开销都太大了,于是把读取内存这一操作优化成读取寄存器这一操作。

        在JMM模型是这样描述的:

        编译器发现,每次循环都要从 “主内存” 中读取数据,就会把数据从 “主内存” 中复制下到 “工作内存” 中,后续每次读取都是在 “工作内容” 这读取。(这里的“工作内容代指cpu寄存器 + 缓存”)---->这里的 “主内存” 翻译成内存,“工作内存” 翻译成cpu寄存器 ;

3.3 线程不安全的原因

1、根本原因
        操作系统上的线程是“抢占式执行”,随机调度的
---->给线程之间的执行顺序带来了很多变数。

2、代码结构
        多个线程同时修改同一个变量。

    2.1、如果一个线程修改一个变量,没事。

    2.2、多个线程读取同一个变量,没事的-->如果只是读取,变量的内容是固定不变的。

    2.3、多个线程修改不同的变量,没事--->如果是两个不同的变量,彼此之间就不会产生相互覆盖的情况了

3、直接原因
        多线程修改操作,本身不是原子的

        即count++:该操作可以被细分为3个cpu指令,一个线程执行这些指令,执行到一半会被调走,从而给其他线程“可乘之机”------>每个cpu指令,都是原子的,要么不执行,要么执行完

4、内存可见性
        一个线程读,一个线程写,也会导致线程安全的问题。

5、指令重排序
        编译器的一种优化,在保证代码逻辑不变的情况下,将一些代码的指令重新排序,从而提高代码的执行效率,但是有时候会因为重排序后,多线程编程就会出现线程安全问题。

番外:

        String是一个不可变对象

好处:

  1. 方便jvm进行缓存(放到字符串常量池中)
  2. Hash值固定
  3. String的对象是线程安全的-à意味着只能读取,不能修改

为什么说string是不可变的?

  1. 持有的数据(char 【】数组)是private的
  2. 里面没有提供public的方法来修改char数组的相关内容。
  3. Final只是表示不可被继承,和可变没有关系。

3.4  针对上述原因给出的解决方案 

        针对原因1:
        我们无法给出解决方案,因为操作系统内部已经实现了“抢占式执行”,我们干预不了

        针对原因2:
        分情况,有的时候,代码结构可以调整,有的时候调整不了。

        针对原因3:
        把要修改的变量这操作,通过特殊手段,把这操作在系统里的多个指令打包成一个“整体”。例如加锁操作,而加锁的操作(下一篇详细讲解),就是把多个指令打包成一个原子的操作。

        针对原因4:
        可以对代码进行调整,避免内存可见性的问题;也可以使用volatile进行修饰,强制把代码优化关了,这样数据就更准确了,但执行效率也就变慢了。

        针对原因5:
        将某些可能会指令重排序的变量,加volatile修饰,强制取消指令重排序的优化。

ps:本次的内容就到这里了,如果感兴趣的话就请一键三连哦!!!文章来源地址https://www.toymoban.com/news/detail-776450.html

到了这里,关于【Java EE初阶三 】线程的状态与安全(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 【Java EE 初阶】文件操作

    目录 1.什么是文件? 1.在cmd中查看指定目录的树形结构语法 2.文件路径 从当前目录开始找到目标程序(一个点) 返回到上一级目录,再找目标程序(两个点) 2.Java中文件操作 1.File概述 1.属性 2. 构造方法 3.常用方法  代码展示: 4.常用方法2 3. 文件内容的读写---数据流 1.I

    2024年02月06日
    浏览(43)
  • 【Java EE初阶十三】网络初识

            网络发展的几个主要时期:         单机时代-局域网时代-广域网时代-移动互联网时代         随着时代的发展,越来越需要计算机之间互相通信,共享软件和数据,即以多个计算机协同工作来完成 业务,就有了网络互连。         网络互连: 将多台计

    2024年02月20日
    浏览(44)
  • Java多线程【状态与安全问题】

    线程状态 说明 NEW 安排了工作, 还未开始行动 RUNNABLE 可工作的. 又可以分成正在工作中和即将开始工作 BLOCKED 这几个都表示排队等着其他事情 WAITING 这几个都表示排队等着其他事情 TIMED_WAITING 这几个都表示排队等着其他事情 TERMINATED 工作完成了 1.NEW状态:安排了工作, 还未开始

    2023年04月09日
    浏览(50)
  • 【Java EE初阶十七】网络原理(二)

    2.2.2 关于可靠传输 4.滑动窗口         前面的三个机制,都是在保证 tcp 的可靠性;         TCP 的可靠传输,是会影响传输的效率的.(多出了一些等待 ack 的时间,单位时间内能传输的数据就少了);         滑动窗口,就让可靠传输对性能的影响,更少一些.TCP 只要引入了可

    2024年02月20日
    浏览(42)
  • 【Java EE初阶十六】网络原理(一)

            在网络原理中主要学习TCP/IP四层模型中的重点网络协议         应用层是和程序员接触最密切的;         应用程序:在应用层这里,很多时候都是程序员自定义应用层协议(步骤:1、根据需求,明确要传输的信息,2、约定好信息按照什么样的格式来组织)的

    2024年02月20日
    浏览(37)
  • 【Java-10】深入浅出线程安全、死锁、状态、通讯、线程池

    线程安全 线程死锁 线程的状态 线程间通讯 线程池 1.1 线程安全产生的原因 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了 问题出现的原因 : 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了 1.2 线程

    2024年02月11日
    浏览(72)
  • 【Java EE初阶二十二】https的简单理解

             当前网络上,主要都是 HTTPS 了,很少能见到 HTTP.实际上 HTTPS 也是基于 HTTP.只不过 HTTPS 在 HTTP 的基础之上, 引入了\\\"加密\\\"机制;引入 HTTPS 防止你的数据被黑客篡改 ;         HTTPS 就是一个重要的保护措施.之所以能够安全, 最关键的在于\\\"加密”;         明文:

    2024年02月22日
    浏览(52)
  • 【Java EE初阶二十一】http的简单理解(二)

            Referer 描述了当前页面是从哪个页面跳转来的,如果是直接在地址栏输入 url(或者点击收藏夹中的按钮) 都是没有 Referer。如下图所示:         HTTP 最大的问题在于\\\"明文传输”,明文传输就容易被第三方获取并篡改.         HTTPS 针对 HTTP 数据进行了加密 (h

    2024年02月22日
    浏览(39)
  • 【Java EE初阶十五】网络编程TCP/IP协议(二)

            tcp的socket api和U大片的socket api差异很大,但是和前面所讲的文件操作很密切的联系         下面主要讲解两个关键的类:         1、ServerSocket:给服务器使用的类,使用这个类来绑定端口号         2、Socket:即会给服务器使用,又会给客户端使用;         

    2024年02月20日
    浏览(51)
  • 码出高效:Java开发手册笔记(线程安全)

        并发与并行的目标都是尽可能快地执行完所有任务。以医生坐诊为例,某个科室有两个专家同时出诊,这就是两个并行任务,其中一个医生,时而问诊,时而查看化验单,然后继续问诊,突然又中断去处理病人的咨询,这就是并发。在并发环境下,由于程序的封闭性全

    2024年02月08日
    浏览(39)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包