【Java系列】详解多线程(三)—— 线程安全(上篇)

个人主页:兜里有颗棉花糖
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创
收录于专栏【Java系列专栏】【JaveEE学习专栏】
本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌
在这里插入图片描述

目录

  • 一、Java线程的六种状态
  • 二、多线程带来的安全问题——线程安全(重点重点)
  • 三、线程不安全问题的原因
  • 四、解决线程不安全问题
    • synchronized关键字
  • 五、总结

一、Java线程的六种状态

就绪状态和阻塞状态是线程的两种常见的状态,而Java中又对线程作了进一步的区分,即Java中总共有六种线程状态:

  • 新建状态(New):线程对象被创建后,但还没有调用start()方法启动线程时,线程处于新建状态。

举个栗子:
在这里插入图片描述
运行结果如下:
在这里插入图片描述

  • 可运行状态(Runnable):就绪状态我们可以理解为两种情况,一种情况是线程正在CPU上运行,另外一种情况就是线程正在排队中,随时可以去CPU上运行。

举个栗子:
在这里插入图片描述
上述代码属于可运行状态中的第二种情况,即线程正在CPU上运行。
代码运行结果如下:
在这里插入图片描述

  • 阻塞状态(Blocked):因为锁的原因产生了阻塞。
  • 等待状态(Waiting):因为调用wait方法产生了阻塞。
  • TIMED_WAITING:因为调用sleep方法产生了阻塞。

举例代码如下:
在这里插入图片描述
运行结果如下:
在这里插入图片描述

  • 守护状态(Terminated):线程已经执行结束了,但是该线程对应的Thread对象还在。

举个栗子:
在这里插入图片描述
代码运行结果如下:
在这里插入图片描述

注意,有的地方java线程分为了七种状态,即把可运行状态(runnable)拆分成了两种状态就绪状态(ready)和运行状态(running)。这个地方也是可以的。

下图是线程状态转移图,请看:
在这里插入图片描述

二、多线程带来的安全问题——线程安全(重点重点)

我们先来通过一段代码来进行线程安全问题的演示:

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

在上述代码运行之前我们先来复习以下之前的lambda表达式的变量捕获问题:线程t1和t2都引用了外部的Counter对象counter,并且在各自的run()方法中对count进行了自增操作。我们虽然对counter对象中的成员变量进行了修改,但是我们并没有修改counter对象本身。注意线程t1和线程t2引用的是counter对象,而不是counter对象中的成员变量,所以这里依然可以触发变量捕获。

好了,现在回归正题,下面是上述代码的运行结果:
在这里插入图片描述
好像结果并非我们所想的那样(两个线程针对同一个变量进行自增操作,结果应该是20000呀,但是…),那我们再运行一次试试呢,请看:
在这里插入图片描述
现在发现,运行结果发生了变化。我们称上述出现的问题为线程安全问题(多线程执行下产生的bug我们一般称之为线程安全问题;如果某段代码在单线程运行下没有问题,在多线程运行下依然没有问题,此时我们就可以称之为线程安全,反之如果出现问题我们就可以称之为线程不安全,正如上述代码而言就是线程不安全。)那现在我们如果让这两个线程单独执行的话,即先执行完线程t1,然后再执行完线程t2,线程就不会出现问题,如下:
在这里插入图片描述

现在我们来分析一下上述代码中的线程安全问题:先来看count++操作,如果站在CPU指令的角度来说,count++操作是要分为三个步骤来进行执行的。第一步:把内存加载到CPU的寄存器中(load);第二步:把寄存器中的数据进行+1;第三步:把寄存器中的数据写回到内存中(save)。
上述的count++操作的三个步骤如果是在两个线程或者多线程并发执行的情况下就有可能会出现问题。
虽然一个CPU核心上只有一个寄存器,但是我们可以视为两个线程或者多个线程各自可以有自己的一组寄存器,即这多个线程可以做到分时复用寄存器的(简单来说这一个寄存器这会可以给这个线程用,另一会可以给其它线程使用,以达到分时复用的效果)。
在这里插入图片描述
代码中的t1线程和t2线程的调度顺序是不确定的,当然两组指令操作的相对顺序也会存在差异,比如下图:
在这里插入图片描述
在这里插入图片描述
当然情况有非常多种,就上述代码而言,由于线程t1和线程t2的调度顺序不同,两组指令的相对顺序存在非常多的差异,所以最终代码运行结果是不确定的。
我们就拿两次count自增操作为例,即一共六个指令操作,那么这六个指令在一定的执行顺序下就可能导致中间的运算结果被覆盖掉。如下图就是一个典型的栗子:
在这里插入图片描述

综上,上述代码的count++执行的20000次自增操作,这两个线程执行count++的操作中有多少次是串行执行的,有多少次的执行结果是被覆盖掉的,我们是不确定的。因为线程的调度执行是随机的,执行过程是抢占式的执行过程,从而导致这两个线程的指令执行顺序产生差异、变化,而这些差异变化就会导致每次程序运行的结果是不同的。当然也会导致线程安全问题。

三、线程不安全问题的原因

  • 根本原因:多个线程的调度顺序是随机的,操作系统采用的是抢占式执行的策略来调度线程的。(我们以往只需要考略代码在一个固定的执行顺序下运行成功即可,但是我们现在要考虑的是代码需要在多线程中的所有执行顺序中都要正确执行代码才可以。)那现在我们能不能想个办法让代码按照多线程的一定执行顺序下进行执行呢?很遗憾,现在我们还不能很好的解决这个问题。(当前主流的操作系统都是按照抢占式的执行顺序来执行的)
  • 代码结构的原因:多个线程同时修改同一个变量,此时容易发生线程安全问题。(一个线程修改一个变量多个线程读取同一个变量多个线程修改多个变量都是没有问题的)。
  • 进行的修改操作不是原子性的(比如上述代码中的count++操作就不是原子的,因为count++操作实质上是三条执行指令来完成的。),反之我们如果我们的修改操作是原子性的,此时代码就不会产生线程安全问题。另外关于原子的:一条java语句不一定是原子的,也不一定只是一条指令,比如count++就不是原子的、=即直接赋值操作就是原子的if=即先判断再赋值操也不是原子的
  • 内存可见性引起的线程安全问题。
  • 指令重排序引起的线程安全问题。

四、解决线程不安全问题

关于如何解决线程不安全问题的话,最主要的一个切入点就是改变修改操作的非原子性,即将我们的修改操作改变成原子的。我们可以通过加锁操作将一组操作给打包成一个操作,即打包成一个原子的操作。
注意:这里的给线程加锁的操作不同于数据库事务的原子操作:事务的原子操作依靠的主要是回滚;而这里的原子操作通过给线程加锁的操作将线程之间进行互斥,即这个线程在执行任务的时候,其它线程是无法执行任务的。简单来说就是通过给线程加锁以实现在同一时刻的多个线程中,只有一个线程在执行任务

如何给count++进行加锁操作呢?
Java中引入了关键字synchronized
如下图:
在这里插入图片描述
我们进入increase方法之后就会加锁(lock),出了increase方法之后就会解锁(unlock)。
在这里插入图片描述

解释上图:当t1加锁之后,t2也尝试进行加锁,但是t2就会进入阻塞等待状态(这里t2的阻塞等待就是把t2的count++操作推迟到后面去执行,直到t1完成了count++操作之后,t2才能执行count++的操作),这个阻塞等待会一直持续到t1解锁之后(即t1解锁之后t2才能进加锁);当t1解锁之后t2就会进行加锁操作。这里就相当于把指令的穿插式的执行变成了线程的串行执行。
上述代码t1的increase方法加减锁操作是要执行10000次的,t2的increase方法的加锁操作能够执行成功取决于t1执行到什么地方:如果t1执行到increase方法内部的话,此时t1的increase方法在占用锁,即t2的increase方法就必须要等待阻塞;直到t1的increase方法执行完释放锁时,t1的increase方法才能加锁成功(其实t1的increase方法占用锁的时间非常短,换言之t2的increase方法的阻塞等待时间非常短)(注意t1线程和t2线程是同时执行的,并不存在说t1执行完才能执行t2线程)。
此时我们来执行代码验证一下,请看:
在这里插入图片描述
最终代码执行结果符合我们的预期。

好了,现在有个问题,上述代码我们通过加锁之后相当于一定程度上把并发执行变成了线程执行了,那多线程的意思又何在呢?又或者来说多线程还又存在的意义吗?
答案是多线程当然有存在的意义了:我们虽然对increase方法进行了加锁操作,但是我们并没有对for循环进行加锁操作。for循环中的i变量是栈上的一个局部变量。而t1和t2两个线程是有两个独立的栈空间的,即两个for循环中的变量并不是同一个变量。既然这样的话,两个线程修改两个变量是不存在线程安全问题的。我们也不需要对i变量进行加锁操作。
综上,上述代码的两个线程中,有一部分的代码是并发执行的,也有一部分代码是串行执行的,此时当然要比单纯的串行执行的效率高啦。

synchronized关键字

synchronized是java给我们提供的加锁的一种方式,synchronized是通过的搭配代码块的方式来进行加锁的(进入代码块就加锁,出来代码块(无论是正常出代码块,还是因为return退出,还是因为抛出异常而退出代码块都能保证正常解锁)就解锁)。
补充一点:C++、python中的加锁方式是两个独立的方法。

synchronized在进行加减锁操作的时候是以对象为维度进行展开的。

使用synchronized的时候,其实是指定了某个具体的对象进行加锁。如下图:
在这里插入图片描述
解释上述代码:当synchronized直接修饰方法的时候其实就相当于上图代码的针对this加锁(直接修饰方法的写法其实就相当于上图代码的的简化写法上面代码中的t1、t2线程就是针对同一个对象进行加锁的
如果两个线程针对同一个对象进行加锁的话就会存在锁竞争/锁冲突的问题(即一个线程加锁成功,另一个线程阻塞等待)。
如果两个线程针对不同的对象进行加锁的话此时就不会出现锁竞争,当然也就不会出现阻塞等待。但是此时两个线程按照也就不会按照串行执行的方式进行count++操作,此时就会存在线程安全问题。

现在我们就两个线程针对不同的对象就行加锁(当然此时会出现线程安全问题)来进行举例:
在这里插入图片描述
运行结果如下:
在这里插入图片描述
此时如果我们再次就两个线程对同一对象进行加锁(当然不会出现线程安全的问题,另外针对的是哪个对象加锁并不是很重要,重要的是两个或者多个线程是不是针对同一个对象进行加锁)的举例:
在这里插入图片描述
运行结果如下:
在这里插入图片描述

五、总结

  • 如果两个线程针对的是同一个对象进行加锁的话,此时就会产生阻塞等待,不会出现线程安全的问题。所以,必须多个线程对同一个对象加锁此时才有意义。
  • 如果两个线程针对的是不同的对象进行加锁的话,此时也就不会出现阻塞等待,会出现线程安全的问题。
  • 如果两个线程中,一个线程加锁了而另一个线程没有加锁,此时依然会出现线程安全的问题(单方面加锁相当于没有加锁)。

好了,本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!
在这里插入图片描述

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

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

相关文章

基于EasyExcel的数据导入导出

前言&#xff1a; 代码复制粘贴即可用&#xff0c;主要包含的功能有Excel模板下载、基于Excel数据导入、Excel数据导出。 根据实际情况修改一些细节即可&#xff0c;最后有结果展示&#xff0c;可以先看下结果&#xff0c;是否是您想要的。 台上一分钟&#xff0c;台下60秒&a…

OSG中几何体的绘制(二)

5. 几何体操作 在本章的前言中就讲到&#xff0c;场景都是由基本的绘图基元构成的&#xff0c;基本的绘图基元构成简单的几何体,简单的几何体构成复杂的几何体&#xff0c;复杂的几何体最终构造成复杂的场景。当多个几何体组合时&#xff0c;可能存在多种降低场景渲染效率的原因…

SQL进阶理论篇(七):B+树的查询及存储机制

文章目录 简介数据库中的存储结构数据库中的页结构从数据页来看B树的查询过程总结参考文献 简介 我们之前已经了解过数据库的B树索引和Hash索引&#xff0c;这些索引信息以及数据记录都是保存在文件里的&#xff0c;确切的说是存储在页结构中。 本节&#xff0c;从我们将了解…

【LeetCode刷题】-- 161.相隔为1的编辑距离

161.相隔为1的编辑距离 方法&#xff1a;一次遍历 首先&#xff0c;我们要确认字符串的长度不会相差太远。如果长度差了2个或更多字符&#xff0c;那么 s 和 t 就不可能是一次编辑之差的字符串。 接下来&#xff0c;我们假设 s 的长度总是短于或等于 t 的长度。如果不是这样&…

cesium 自定义贴图,shadertoy移植教程。

1.前言 cesium中提供了一些高级的api&#xff0c;可以自己写一些shader来制作炫酷的效果。 ShaderToy 是一个可以在线编写、测试和分享图形渲染着色器的网站。它提供了一个图形化的编辑器&#xff0c;可以让用户编写基于 WebGL 的 GLSL 着色器代码&#xff0c;并实时预览渲染结…

大数据存储技术(3)—— HBase分布式数据库

目录 一、HBase简介 &#xff08;一&#xff09;概念 &#xff08;二&#xff09;特点 &#xff08;三&#xff09;HBase架构 二、HBase原理 &#xff08;一&#xff09;读流程 &#xff08;二&#xff09;写流程 &#xff08;三&#xff09;数据 flush 过程 &#xf…

配置802.1x认证

实验目的&#xff1a; 某公司拥有两个部门&#xff0c;市场部和人事部门&#xff0c;市场部和人事部的IP地址分别为10.1.11.0/24、10.1.21.0/24两个IP网段。市场部属于vlan11&#xff0c;人事部属于vlan21。现在需要在SW2上配置802.1x认证&#xff0c;实现终端用于只有认证成功…

BKP 备份寄存器 RTC 实时时钟-stm32入门

这一章节我们要讲的主要内容是 RTC 实时时钟&#xff0c;对应手册&#xff0c;是第 16 章的位置。 实时时钟这个东西&#xff0c;本质上是一个定时器&#xff0c;但是这个定时器&#xff0c;是专门用来产生年月日时分秒&#xff0c;这种日期和时间信息的。所以学会了 STM32 的…

Flink系列之:SQL提示

Flink系列之&#xff1a;SQL提示 一、动态表选项二、语法三、例子四、查询提示五、句法六、加入提示七、播送八、随机散列九、随机合并十、嵌套循环十一、LOOKUP十二、进一步说明十三、故障排除十四、连接提示中的冲突案例十五、什么是查询块 SQL 提示可以与 SQL 语句一起使用来…

【卡塔尔世界杯数据可视化与新闻展示】

卡塔尔世界杯数据可视化与新闻展示 前言数据获取与处理可视化页面搭建功能实现新闻信息显示详情查看登录注册评论信息管理 创新点结语 前言 随着卡塔尔世界杯的临近&#xff0c;对于足球爱好者来说&#xff0c;对比赛的数据分析和新闻报道将成为关注的焦点。本文将介绍如何使用…

腾讯云优惠全站搜——云服务器配置大全精准

腾讯云推出优惠全站搜页面 https://curl.qcloud.com/PPrF9NFe 在这个页面可以一键查询所需云服务器、轻量应用服务器、数据库、存储、CDN、网络、安全、大数据等云产品优惠活动大全&#xff0c;活动打开如下图&#xff1a; 腾讯云优惠全站搜——优惠合集 如上图&#xff0c;在这…

uniGUI学习之UniTreeview

UniTreeview中能改变一级目录的字体和颜色 function beforeInit(sender, config) { ID"#"config.id; Ext.util.CSS.createStyleSheet( ${ID} .x-tree-node-text{color:green;font-weight:800;} ${ID} .x-tree-elbow-line ~ span{color:black;font-weight:400;} ); }

macbookpro 2024怎么恢复出厂设置

可能你的MacBook曾经是高性能的代表&#xff0c;但是现在它正慢慢地逝去了自己的光芒&#xff1f;随着逐年的使用以及文件的添加和程序的安装&#xff0c;你的MacBook可能会开始变得迟缓卡顿&#xff0c;或者失却了以往的光彩。如果你发现你的Mac开始出现这些严重问题&#xff…

Windows设备管理

1、前言 熟悉Windows系统的都应该使用过设备管理器。设备管理器将操作系统中所有已安装的设备分类展现出来。同时提供了安装、卸载、启用和禁用的功能。 那么&#xff0c;我们应该如何通过C编程的方式实现这种功能呢&#xff1f;答案很简单&#xff0c;那就是使用SetupDi函数族…

GZ015 机器人系统集成应用技术样题4-学生赛

2023年全国职业院校技能大赛 高职组“机器人系统集成应用技术”赛项 竞赛任务书&#xff08;学生赛&#xff09; 样题4 选手须知&#xff1a; 本任务书共 25页&#xff0c;如出现任务书缺页、字迹不清等问题&#xff0c;请及时向裁判示意&#xff0c;并进行任务书的更换。参赛队…

2020 ICPC·小米邀请赛 决赛 J. Rikka with Book(状压dp)

题目 登录—专业IT笔试面试备考平台_牛客网 n(n<20)本书&#xff0c;放在桌子上&#xff0c; 第i本书的可以看成是li(li<1e3)*1*1的物体&#xff0c;其中长为li&#xff0c;宽为1&#xff0c;高为1&#xff0c; 质量均匀分布&#xff0c;且为wi(wi<1e3) 求n本书摞…

前端桌面通知(Desktop Notifications)API

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

msvcrtd.dll下载安装方法,解决msvcrtd.dll找不到的问题

在这篇文章中&#xff0c;我们将详细讨论msvcrtd.dll文件的下载安装方法&#xff0c;并分析出现找不到msvcrtd.dll的情况及解决方法。如果你遇到了与msvcrtd.dll相关的问题&#xff0c;本文将为你提供全面且详细的解决方案。 一.什么是msvcrtd.dll文件 首先&#xff0c;让我们…

JS中的String常用的实例方法

splice():分隔符 把字符串以分隔符的形式拆分为数组 const str pink,red;const arr str.split(,);console.log(arr);//Array[0,"pink";1:"red"]const str1 2022-4-8;const arr1 str1.split(-);console.log(arr1);//Array[0,"2022";1:"…

权重衰减(Weight Decay)

在深度学习中&#xff0c;权重衰减&#xff08;Weight Decay&#xff09;是一种常用的正则化技术&#xff0c;旨在减少模型的过拟合现象。权重衰减通过向损失函数添加一个正则化项&#xff0c;以惩罚模型中较大的权重值。 一、权重衰减 在深度学习中&#xff0c;模型的训练过程…