【后端开发】JavaEE初阶—线程安全问题与加锁原理(超详解)

news2024/9/24 21:01:01

前言:

🌈上期博客:【后端开发】JavaEE初阶—Theard类及常见方法—线程的操作(超详解)-CSDN博客

🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

🌈小编会在后端开发的学习中不断更新~~~

 

目录

📚️1.引言

📚️2.线程状态

📚️3.线程安全

3.1什么是线程安全

3.2线程安全问题原理

3.3线程安全问题原因

📚️4.实现加锁

4.1加锁的原因

4.2加锁的目的

 4.3加锁的实现

5.加锁的注意事项

6.加锁的其他写法

 📚️5.总结


📚️1.引言

 Hello!!!小伙伴们,小编上期讲解了关于Tread类的相关知识解析,以及对于线程的相关操作,相信大家对于这类知识有了新的理解,本期将讲解关于线程的重点问题,即关于线程安全和加锁的理解;开始发车了gogogo~~~🥳🥳🥳;

且听小编讲解,包你学会!!! 

📚️2.线程状态

关于线程状态,我们之前讲解到,线程大致有两种状态

就绪状态:表示这个线程随时可以实现调度去CPU上执行,并且包括已经在CPU上执行的线程;

阻塞状态:即这个线程不方便去CPU上进行执行,即不方便调度,并且在java中对于阻塞有几种状态;

几种状态:

NEW:表示Thread对象创建后,但是还没有调用start方法在系统内创建线程

RUNNABLE:表示线程进入就绪状态,线程准备被CPU调度,或者已经在CPU上执行

TERMINATED:表示系统内的线程已经被执行了,线程已经销毁,但是Thread类对象还存在
TIMED_WAITING:指时间阻塞,到达一定的时间后解除阻塞,主要是在Sleep的休眠等待阻塞

WAITING:即不带有时间的阻塞(死等),只有达到一定的条件后,才会解除阻塞,主要是join,wait;

BLOCKED:即产生的锁竞争,引起的阻塞;后面现编会讲到

以上小编WAITING和BLOCKED小编就后面介绍

对于上述的概念比较难懂,那么接下来,小编用代码为大家进行实现,讲解吧~~~

代码实现:

public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            System.out.println("线程执行中");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("线程状态"+ thread.getState());//NEW
        thread.start();
        System.out.println("线程状态"+ thread.getState());//RUNNABLE
        Thread.sleep(2000);//确保线程执行完
        System.out.println("线程状态"+thread.getState());//TERMINATED
    }

讲解:线程start方法之前,线程还没有被创建,所以这里就是NEW,在线程创建后,此时线程就为就绪状态,此时就是RUNNABLE,执行完后,就是TERMINATED;

当然那还有TIMED_WAITING状态,代码如下:

thread.start();
Thread.sleep(2000);//确保线程进入休眠
System.out.println("线程状态"+thread.getState());//TIMED_WAITING

讲解:这里进行主线程的休眠是为了确保线程进入休眠等待状态,此时状态就TIMED_WAITING

 图解实例:

注意:红色框里为线程的一般执行过程,若在加入了一些相关指令,没那么对应的线程状态也要进行改变;

📚️3.线程安全

实现多线程编程是为了实现并发线程,但是实现并发编程并不只有多线程才能实现~~~

多线程编程是比较原始,但是比较朴素的一种实现并发编程的方法

3.1什么是线程安全

当一个任务在单线程的执行下,或者在多个线程的执行下,没有出现BUG的情况下,那么就是线程安全的;

如果一个任务在单线程的执行下是没有BUG的,但是在多个线程的执行下又出现了BUG,那么此时就是线程不安全的;

 例如如下代码:

 public static int num=0;
    //线程安全问题
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {            
                    num++;
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {               
                   num++;               
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("num="+num);
    }

注解:可以看到,我们这里实现的两个线程来进行num的递增,当两个线程启动后,我们的预料输出就是10万,但是输出如下:

此时可以看到这里和我们的预期输出是不一样的,那么这是为什么呢???

3.2线程安全问题原理

在上述问题中,我们发现此时在实现变量自增时,此时的就发生了线程的安全问题~~~

那么此时我们就要从CPU指令原理来理解了:

注意:这个执行语句num++,实际是由cpu上的三个指令构成的;

📍load:表示从内存中读取数据到cpu寄存器中;

📍add:表示把寄存器中的值加一;

📍save:表示将寄存器的数据写回到内存当中;

 又因为线程的随机调度的问题,此时就有一下几种情况:

第一种情况;

那么此时我们就可以看到,这里的执行结果就符合我们数值递增的规律,那么此时又因为线程的随机调度,和抢占式执行,那么大多数就是按照以下情况执行:

第二种情况:

  

通过上述的图片解析,我们有以下了解:

1.在多线程实现时,存在随机调度的问题,那么此时cpu的三个指令会随机调度到cpu上去执行,此时就存在线程安全问题;

2.在随机调度的上述两中情况是不一定的,还存在无数种;

3.线程安全的前提是第一个线程成功save数据回到内存中后,线程二再load读取数据后,才能保证线程安全,但是这是几率很小的,大多数都是第二种类似的情况

由于小编画图有限,这里的第二种情况还有很多,大家可以自己试试边画边理解哦;

3.3线程安全问题原因

在上述的总结实现后,小编总结了一下三点问题;

📍(根本原因)由于线程的随机调度,和抢占式执行的,导致了线程之间执行的顺序是不确定的,是有无数种情况的;

📍(代码结构)即多个线程同时执行一个任务,那么此时就会存在线程安全问题

📍(直接原因)上述的num++操作本来就不是“原子的”,即最小执行单元,要么不执行,要么执行完

📍还有内存可见性以及指令重排序的问题,这里代码没有涉及,小编就不再讲解了(小编也不知道😁😁😁)

📚️4.实现加锁

4.1加锁的原因

在上述讲解中,我们了解到线程的安全问题就是由于线程随机调度,导致的执行顺序不确定;

那么对于上述的几种原因,我们能够控制的就是(代码结构)(直接原因)但是为了实现这种代码结构,就会切断提高执行效率的要求,所以我们只能从直接原因入手;

4.2加锁的目的

加锁的目的就是为了实现多个指令,打包成一个原子的操作;

加锁后线程任然要进行随机的调度,但是此时即时在执行的线程被调度走了,但是其他线程仍然不会插队进行执行;

如图:

注意:这里的lock和unlock实现加锁后,添加的两条指令,在java中使用synchronized即可实现;

此时就解决了线程的随机调度和抢占式执行产生的线程安全的问题了~~~

 4.3加锁的实现

当我们要实现加锁的操作时,那么就要添加锁对象,加锁解锁的操作都是依靠这个锁对象来执行的

 代码实现如下:

 public static int num=0;    
    public static void main(String[] args) throws InterruptedException {
        Object ram=new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                加锁
                synchronized (ram){
                    num++;
                }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (ram) {
                    num++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("num="+num);
    }

此时可以发现,小编设置了一个Objec类的对象,这里只要是对象就都可以,运用synchronized实现对于要进行加锁代码的操作;

注意:如果一个线程针对一个锁对象进行加锁后,其他线程也针对这个锁对象进行加锁,就会发生阻塞(BLOCKED)直到前一个线程执行完后即释放锁

5.加锁的注意事项

1.当线程加锁,另一个线程不加锁的时候

Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                加锁
                synchronized (ram){
                    num++;
                }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {                
                    num++;           
            }
        });

这里就是一个加锁,另一个不加锁的情况,此时仍然存在线程安全问题;

注意:当写成上述情况时,此时两个线程就不会存在锁竞争了,那么对应的也就没有了线程阻塞问题,此时仍然会因为线程调度,导致线程安全问题;

2,当加锁的对象不同

Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                
                synchronized (ram){
                    num++;
                }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (tam) {
                    num++;
                }
            }
        });

那么此时和上述情况一样,不会存在锁竞争,不会产生阻塞;

3.在加锁时要保证加锁对象一样

除了上述的写法之外,我们还能够用方法来实现:

 public static void main(String[] args) throws InterruptedException {
        Test t=new Test();
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                t.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                t.add();
            }
        });
        //线程启动,与等待
    }
}
class Test{
    public int num=0;
    public void add(){
        synchronized (this){
            num++;
        }

    }

小编这里省去了线程的启动和等待哦~~~

这里就运用了方法实现线程的加锁,这里的this代表的是创建这个类的对象实例,那么此时两个线程调用方法的对象一样,那么此时也是线程安全的;

注意:综上所述,在解决线程安全问题时,主要是实现线程阻塞,加锁;那么加锁的对象一定要是一致的,否则会导致无法产生线程锁竞争,从而产生线程安全问题

6.加锁的其他写法

在实现加锁的操作时,对于以上的加锁方式,我们可以实现改进;

这里synchronized(this)代码中,我们就可以将synchronized写到方法上,代码如下:

class Test {
    public int num = 0;

    synchronized public void add() {
        num++;
    }
}

这种写法和上述的写法作用效果是一样的;

对于当括号里面是类对象时:

class Test {
    public int num = 0;

     public  void add() {
        synchronized (Test.class){
            num++;
        }
    }
}

注意:在一个Java进程中只有一个类对象,所以第一个进程拿到的类对象和第二个进程拿到的类对象是同一个对象,此时就满足阻塞的要求;

当synchronized修饰静态方法时:

class Test {
    public static int num = 0;
     public static void add() {
         synchronized (Test.class){
             num++;
         }
        
    }
}

 这个写法可以被代替成:

synchronized public static void add() {
        num++;
    }

注意:如果synchronized是加在static静态方法上就相当于给类对象加锁;

 📚️5.总结

💬💬本期小编总结了关于多线程编程的重要问题,线程状态,以及线程安全问题讲解,并且如何解决线程安全问题,进行了原理讲解,并提出加锁的概念和实现;还附上了代码加以讲解~~~

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                                                               😊😊  期待你的关注~~~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

综合体第三题(DHCP报文分析)

DHCP&#xff08;一般情况下&#xff09; 某公司网络DHCP服务器地址为192.168.0.2&#xff0c;可分配IP地址为192.168.0.6-192.168.0.254&#xff0c;缺省网关的地址为192.168.0.1&#xff0c;DNS服务器地址为192.168.0.2。网络中某客户机从服务器获取IP地址后&#xff0c;在客…

初识爬虫8

1.selenium的作用和工作原理 2. 使用selenium&#xff0c;完成web浏览器调用 # -*- coding: utf-8 -*- # 自动化测试工具&#xff0c;降低难度&#xff0c;性能也降低 from selenium import webdriverdriver webdriver.Edge()driver.get("https://www.itcast.cn/")…

Python语言基础教程(上)4.0

✨博客主页&#xff1a; https://blog.csdn.net/m0_63815035?typeblog &#x1f497;《博客内容》&#xff1a;.NET、Java.测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识 &#x1f4e2;博客专栏&#xff1a; https://blog.csdn.net/m0_63815035/cat…

新规2027年生效 美国禁止中国智能网联汽车软硬件

当地时间9月23日&#xff0c;美国商务部工业和安全局&#xff08;BIS&#xff09;发布了一项拟议规则制定通知&#xff08;NPRM&#xff09;&#xff0c;该通知将禁止销售或进口集成特定硬件和软件的联网车辆&#xff0c;或单独销售这些组件&#xff0c;这些硬件和软件与中国或…

增强网络威胁防御能力的云安全新兴技术

一些行业专家强调了基于云的运营的独特网络安全需求&#xff0c;并指出保护敏感数据与传统的本地网络不同。尽管新兴技术并没有改变网络安全专业人员与犯罪分子之间持续的斗争&#xff0c;但它们提高了赌注&#xff0c;使斗争变得更加复杂。 如今&#xff0c;我们面对的是技术…

CSS | 如何来避免 FOUC(无样式内容闪烁)现象的发生?

一、什么是 FOUC(无样式内容闪烁)? ‌FOUC&#xff08;Flash of Unstyled Content&#xff09;是指网页在加载过程中&#xff0c;由于CSS样式加载延迟或加载顺序不当&#xff0c;导致页面出现闪烁或呈现出未样式化的内容的现象。‌ 这种现象通常发生在HTML文档已经加载&…

为什么数据需要 QA 流程

当有人问我做什么工作时&#xff0c;我会说我是一名数据质量保证 (QA) 工程师。他们并不真正理解我的意思。“嗯&#xff0c;我做数据测试&#xff0c;”我试图解释&#xff0c;但常常无济于事。我有一些从事技术和软件开发的朋友&#xff0c;他们不太了解数据测试是什么&#…

[LLM 学习笔记] Transformer 基础

Transformer 基础 Transformer 模型架构 主要组成: Encoder, Decoder, Generator. Encoder (编码器) 由 N N N 层结构相同(参数不同)的 EncoderLayer 网络组成. In : [ b a t c h _ s z , s e q _ l e n , d m o d e l ] \textbf{In}: [batch\_sz, seq\_len, d_{model}] I…

【WorldView系列卫星】

WorldView系列卫星 WorldView系列卫星是美国DigitalGlobe公司推出的一系列先进商业遥感卫星&#xff0c;旨在提供高分辨率的地球成像服务。该系列卫星以其卓越的成像能力&#xff0c;如高分辨率、快速重访时间和宽幅扫描能力&#xff0c;引领了地球观测技术的新标准。以下是对…

LCD1602

LCD1602 是一种工业字符型液晶显示屏&#xff0c;能够同时显示 16x2 即 32 个字符。 LCD的显示控制 通过向 LCD1602 发送指令和数据来控制其显示内容。指令包括清屏、设置光标位置、显示模式等&#xff1b;数据则是要显示的字符的 ASCII 码。LCD1602 内部有一个控制器&#x…

el-form动态标题和输入值,并且最后一个输入框不校验

需求&#xff1a;给了固定的label&#xff0c;叫xx单位&#xff0c;要输入单位的信息&#xff0c;但是属性名称都一样的&#xff0c;UI画图也是表单的形式&#xff0c;所以改为动态添加的形式&#xff0c;实现方式也很简单&#xff0c;循环就完事了&#xff0c;连着表单校验也动…

ChatGLM-6B:部署指南与实战应用全解析

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 SD3ComfyUI文生图部署步骤DAMODEL-ChatGLM-6B 服务端部署1.1、实例创建1.2、模型准备1.3、模型启动 SD3ComfyUI文生图部署步骤 Chat…

centos系统配置本地镜像源

今天在安装docker相关的依赖包时&#xff0c;在centos系统上&#xff0c;发现很多原本的镜像地址&#xff0c;拉取rpm依赖包失败&#xff0c;因为我的centos系统&#xff0c;安装的时候使用的是最小化安装&#xff0c;很多rpm包没有安装好&#xff0c;所以在后续安装一些常规命…

电脑共享同屏的几种方法分享

实时共享屏幕的方法多种多样&#xff0c;适用于不同的设备和场景。以下是几种常见的实时共享屏幕的方法&#xff1a; 1. 使用无线投屏技术 原理&#xff1a;无线投屏技术允许设备通过Wi-Fi网络或其他无线连接方式&#xff0c;将屏幕内容实时投射到另一台设备上。 操作步骤&a…

如何分析开源项目

如何分析开源项目 教程&#xff1a;【狂神说Java】手把手教你如何分析开源项目_哔哩哔哩_bilibili 一.开源项目下载 1.下载 因为GitHub下载太慢了&#xff0c;下载完毕进行解压。 后端&#xff1a;eladmin: eladmin-jpa 版本&#xff1a;项目基于 Spring Boot 2.1.0 、 Jpa、…

如何在openEuler上安装和配置openGauss数据库

本文将详细介绍如何在openEuler 22.03 LTS SP1上安装和配置openGauss数据库&#xff0c;包括数据库的启动、停止、远程连接配置等关键步骤。 1、安装 使用OpenEuler-22.03-LTS-SP1-x64版本的系统&#xff0c;通过命令行安装openGauss数据库。 1.1、确保系统软件包索引是最新…

Anaconda安装保姆级教程

1.下载Anaconda 可以在官网下载地址链接: Anaconda官网 推荐&#xff1a;清华大学镜像 2.安装 推荐&#xff0c;允许其他程序&#xff0c;如VSCode、PyCham等自动检测anaconda作为系统上的主Python。 3.配置环境变量 D:\Anaconda3为安装路径 D:\Anaconda3 D:\Anaconda3\S…

HT6819 3.3W带数字音量控制/防削顶低EMI立体声D类音频功率放大器

特点 降低了内阻和热耗的EDMA专利技术&#xff0c;极大提升了 输出功率和产品可靠性 Po3.3 W(VDD5.0V,RL4Ω,THDN10%) Po1.7 W(VDD3.6V,RL4Ω,THDN10%) 优异的全带宽EMI抑制性能 a)独创的主动边沿控制专利(AROC&#xff0c;Active edge Ringing and Overshoot Control circuitr…

monaco-editor基本使用

前言 公司项目需要代码编辑器&#xff0c;多方参考之后用了monaco-editor。 一、monaco-editor是什么&#xff1f; Monaco Editor 是微软开源的基于 VS Code 的代码编辑器&#xff0c;运行在浏览器环境中。 二、使用步骤 1.npm下载插件 //我下载的版本 npm i monaco-edit…

Java_Day05学习

Object类被子类经常重写的方法 方法说明toString()返回当前对象本身的有关信息&#xff0c;按字符串对象返回equals()比较两个对象是否是同一个对象&#xff0c;是则返回****truehashCode()返回该对象的哈希代码值getClass()获取当前对象所属的类信息&#xff0c;返回Class对象…