什么是线程安全?如何保证线程安全?

目录

一、引入线程安全 👇

二、 线程安全👇

1、线程安全概念 🔍

2、线程不安全的原因 🔍

抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

多个线程修改同一个变量 

 修改操作,不是原子的(不可分割的最小单位) 

内存可见性,引起的线程不安全 

指令重排序,引起的线程不安全

三、解决之前的线程不安全问题👇

1、synchronized 关键字-监视器锁monitor lock  🔍

1)synchronized 的特性 

(1) 互斥

(2)刷新内存

(3) 可重入

2)synchronized 使用示例 

1) 直接修饰普通方法:

 2) 修饰静态方法:

3) 修饰代码块: 明确指定锁哪个对象.

2、volatile 关键字 (保证内存可见性) 🔍

 1)引入volatile

2)volatile不保证原子性

💡 总结:


一、引入线程安全 👇

执行以下代码:

package threading;
class Counter {
    public int count = 0;
    void increase() {
        count++;
    }
}

public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        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);
    }
}

可以观察代码,我们对两个线程分别累加50000次,结果应该为100000,但是大家看运行结果并非如此,而且可以发现,每次运行的结果都不一样,这是什么原因呢? 

答案就是涉及到了线程安全问题

 

 

本质原因:线程在系统中的调度是无序的/随机的(抢占式执行的) 


二、 线程安全👇

1、线程安全概念 🔍

    如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。 

2、线程不安全的原因 🔍

  • 抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

  • 多个线程修改同一个变量 

一个线程修改同一个变量=>安全

多个线程读取同一个变量=>安全

多个线程修改不同变量=> 安全 

  •  修改操作,不是原子的(不可分割的最小单位) 

什么是原子性🐶

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?🐶

是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的: 1. 从内存把数据读到 CPU 2. 进行数据更新 3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题🐶

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.

  • 内存可见性,引起的线程不安全 

          可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

  • 指令重排序,引起的线程不安全


三、解决之前的线程不安全问题👇

1、synchronized 关键字-监视器锁monitor lock  🔍

1)对文章初始代码进行修改:既可以保证 ++ 操作就是原子的,不受影响啦

可以将{ }视为厕所,进表示加锁,出表示解锁

void increase() {
        //锁有俩个核心操作,加锁和解锁
        //进入该代码块就会触发加锁,出了代码块,就会触发解锁
        synchronized (this) {
            count++;
        }
    }

 注意:此处的this指的就是counter对象

因此,在上述代码中,两个线程实在竞争同一个锁对象,就会产生锁竞争。

再执行上述代码:就是100000

疑惑为啥以上操作为什么可以解决线程安全问题呢? 🐶

1)synchronized 的特性 

(1) 互斥

      synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁 

理解 "阻塞等待". 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁.

注意: 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则. 

(2)刷新内存

synchronized 的工作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁 所以 synchronized 也能保证内存可见性. 
(3) 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; 

理解 "把自己锁死" :一个线程没有释放锁, 然后又尝试再次加锁. 

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁

当然,Java 中的 synchronized 是 可重入锁, 因此没有上面的问题

 示例:

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

increase 和 increase2 两个方法都加了 synchronized,

此处的 synchronized 都是针对 this 当前对象加锁的.

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.

注意:在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.

解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到) 

2)synchronized 使用示例 

 synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具 体的对象来使用.

1) 直接修饰普通方法:

锁的 SynchronizedDemo 对象🐶

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
 2) 修饰静态方法:

锁的 SynchronizedDemo 类的对象🐶

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
3) 修饰代码块: 明确指定锁哪个对象.

锁当前对象🐶

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

锁类对象🐶

类对象是啥:Counter.class

.java源代码文件,javac =>.class(二进制字节码文件),JVM就可以执行.class文件了,类对象就可以表示这个.class文件的内容~~(描述了类的方方面面的详细信息,比如诶的名字,类的属性,类的方法,)

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

2、volatile 关键字 (保证内存可见性)🔍

 1)引入volatile

所谓内存可见性,就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了代码的bug

因此我们可以加上 volatile(让编译器对这个场景暂停优化) , 强制读写内存. 速度是慢了, 但是数据变的更准确了.每次都是从内存中重新读取数据

观察以下代码:

package threading;

import java.util.Scanner;

public class ThreadDemo24 {
    public static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

            }
            System.out.println("循环结束,t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            //t2通过控制台输入一个整数,yidanyonghushurule非0的值,此时t1的循环就会立即结束,从而t1线程就会退出
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 我们预期的效果应该是输入一个非零的数,线程t1就会停止,但实际上仍然在执行,处在RUNNABLE状态

 为什么会出现以上问题呢?内存可见性的锅!!!

让我们分析一下代码:

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度 非常快, 但是可能出现数据不一致的情况.

 

volatile public static int flag = 0;

加上volatile关键字,此时编译器就可以保证每次都是重新从内存读取flag变量的值,

此时t2修改flag,t1就可以立即感知到了,t1就可以正确退了

2)volatile不保证原子性

这个是最初的演示线程安全的代码.

给 increase 方法去掉 synchronized

给 count 加上 volatile 关键字.

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    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);
}

此时可以看到, 最终 count 的值仍然无法保证是 100000 

💡 总结:
  • volatile不保证原子性
  • volatile也能禁止指令重排序
  • volatile 适用一个线程读,一个线程写的情况
  • synchronized则是多个线程写 
  • volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性(也能保证内存可见性), volatile 保证的是内存可见 性
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
  if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

上面代码:

去掉 flag 的 volatile

给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁. 

运行结果是可以正常结束的

因此 synchronized是可以保证内存可见性的

 

 补充: 指令重排序,也是编译器优化的策略,调整了代码的执行顺序,让程序更高效,前提也是保证整体逻辑不变

 💡 总结:(面试题)

线程不安全的原因:

【根本原因】==操作系统上的线程是“抢占式执行”的,线程调度是随机的,==这是线程不安全的一个主要原因。随机调度会导致在多线程环境下,程序的执行顺序不确定,程序员必须确保无论哪种执行顺序,代码都能正常运行。
【代码结构】共享资源:多个线程同时访问并修改共享的数据或资源。当多个线程同时访问和修改共享资源时容易引发竞态条件和数据不一致的问题。
①一个线程修改一个变量是安全的
②多个线程修改一个变量是不安全的
③多个线程修改不同变量是安全的
【直接原因】多线程操作不是“原子的”。多线程操作中的原子性指的是一个操作是不可中断的,要么全部执行完成,要么都不执行,不能被其他线程干扰。这对于并发编程非常重要,因为如果一个操作在执行过程中被中断,可能导致数据不一致或者其他意外情况发生。(在上述多线程操作中,count++操作不是“原子的”,而是由多个CPU指令组成的,一个线程执行这些指令时,可能会在执行过程中被抢占,从而给其他线程“可乘之机”。要保证原子性操作,每个CPU指令都应该是“原子的”,即要么完全执行,要么完全不执行。)
内存可见性问题:在多线程环境下调用不可重入的函数(即不支持多线程调用的函数),可能导致数据混乱或程序崩溃。
指令重排序问题:在多线程环境下,由于编译器或处理器对指令进行重排序优化,可能导致预期之外的程序行为。

 

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。


                        

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

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

相关文章

Java代码审计-XSS审计

一、漏洞简介 XSS是Cross Site Scripting的缩写&#xff0c;意为"跨站脚本攻击"&#xff0c;为了避免与层叠样式表(Cascading Style Sheet&#xff0c;CSS)的缩写混淆&#xff0c;故将跨站脚本攻击缩写为XSS。XSS是一种针对网站应用程序的安全漏洞攻击技术&#xff…

线上申请流量卡一些必知的小知识,愿每个人都能刷到!

很多朋友都想办理一张大流量卡&#xff0c;但是又怕被套路&#xff0c;一时不知道该怎么选择&#xff0c;那个纠结啊。 今天&#xff0c;小编用自己多年的行业经验给大家整理了一些办卡攻略&#xff0c;希望能帮助大家选到适合自己的流量卡。 ​1、有的流量卡都是免费申请&…

使用JavaScript日历小部件和DHTMLX Gantt的应用场景(三)

DHTMLX Suite UI 组件库允许您更快地构建跨平台、跨浏览器 Web 和移动应用程序。它包括一组丰富的即用式 HTML5 组件&#xff0c;这些组件可以轻松组合到单个应用程序界面中。 DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表&#xff0c;可满足项目管理应用…

了解 Linux 网络卡绑定:提高网络性能与冗余性

在现代 IT 基础设施中&#xff0c;网络性能和可靠性至关重要。对于许多企业和个人用户来说&#xff0c;确保网络的高可用性和冗余性是首要任务之一。Linux 提供了一个强大的解决方案——网络卡绑定&#xff08;Network Interface Card Bonding&#xff0c;简称 NIC Bonding&…

DevExpress Office File API中文教程 - 如何用OpenAI模型增强Office文档可访问性?

DevExpress Office File API是一个专为C#, VB.NET 和 ASP.NET等开发人员提供的非可视化.NET库。有了这个库&#xff0c;不用安装Microsoft Office&#xff0c;就可以完全自动处理Excel、Word等文档。开发人员使用一个非常易于操作的API就可以生成XLS, XLSx, DOC, DOCx, RTF, CS…

58同城如何降低 80%的机器成本 | OceanBase案例

本文作者&#xff1a;58同城架构师刘春雷 一、背景介绍 58同城作为中国互联网生活服务领域的领军者&#xff0c;其平台规模居国内之首&#xff0c;涵盖了包括车辆交易、房产服务、人才招聘、本地生活服务以及金融等多元化的业务场景。 因其业务的广泛性和多样性&#xff0c;我…

Keil MDK map文件学习笔记

Keil MDK map文件学习笔记 map文件组成1.Section Cross References段交叉引用2.Removing Unused input sections from the image移除无用的段3.Image Symbol Table镜像符号表局部符号表全局符号表 4.Memory Map of the image镜像存储器映射ROM区执行域RAM区执行域 5. Image com…

DLRover:蚂蚁集团开源的AI训练革命

在当前的深度学习领域&#xff0c;大规模训练作业面临着一系列挑战。首先&#xff0c;硬件故障或软件错误导致的停机时间会严重影响训练效率和进度。其次&#xff0c;传统的检查点机制在大规模训练中效率低下&#xff0c;耗时长且容易降低训练的有效时间。资源管理的复杂性也给…

关于新配置的adb,设备管理器找不到此设备问题

上面页面中一开始没有找到此android设备&#xff0c; 可能是因为我重新配置的adb和设备驱动&#xff0c; 只把adb配置了环境变量&#xff0c;驱动没有更新到电脑中&#xff0c; 点击添加驱动&#xff0c; 选择路径&#xff0c;我安装时都放在了SDK下面&#xff0c;可以尝试…

卷爆短剧出海:五大关键,由AIGC重构

短剧高温下&#xff0c;谈谈AIGC的助攻路线。 短剧&#xff0c;一个席卷全球的高温赛道。 以往只是踏着霸总题材&#xff0c;如今&#xff0c;内容循着精品化、IP化的自然发展风向&#xff0c;给内容、制作、平台等产业全链都带来新机&#xff0c;也让短剧消费走向文化深处&am…

【C语言回顾】动态内存管理

前言1. 动态内存管理初步概述2. malloc3. calloc4. realloc5. free6. 常见的动态内存错误7. 柔性数组8. 程序内存区域划分结语 #include<GUIQU.h> int main { 上期回顾: 【C语言回顾】联合和枚举 个人主页&#xff1a;C_GUIQU 专栏&#xff1a;【C语言学习】 return 一键…

win32-鼠标消息、键盘消息、计时器消息、菜单资源

承接前文&#xff1a; win32窗口编程windows 开发基础win32-注册窗口类、创建窗口win32-显示窗口、消息循环、消息队列 本文目录 键盘消息键盘消息的分类WM_CHAR 字符消息 鼠标消息鼠标消息附带信息 定时器消息 WM_TIMER创建销毁定时器 菜单资源资源相关菜单资源使用命令消息的…

远动通讯屏具体干啥作用

远动通讯屏具体干啥作用 远动通讯屏主要用于电力系统中的各类发电厂、变电站、光伏电站、开闭所、配电房等&#xff0c;具有实时传输数据和远程控制功能。它的主要作用包括&#xff1a; 数据采集&#xff1a;远动通讯屏能够采集各种模拟量、开关量和数字量等信息&#xff0c…

python查找内容在文件中的第几行(利用了滑动窗口)

def find_multiline_content(file_path, multiline_content):with open(file_path, r) as file:# 文件内容file_lines file.readlines()# 待检测内容multiline_lines multiline_content.strip().split(\n)# 待检测内容总行数num_multiline_lines len(multiline_lines)matchi…

【CALayer-CALayer的transform属性 Objective-C语言】

一、接下来,我们来说的是这个,transform的属性 1.layer的transform属性, 把最后一份代码command + C、command + V、一份儿,改个名字, Name:04-CALayer的transform属性, 我们把这个代码稍微修改一下, 我们先添加了一个layer,到控制器的view上, 然后呢,这两句话不…

Tina-Linux -- 5. 网络通信(有线网络,无线网络,SSH链接)

有线网络 bash 指令 ifconfig eth0 192.168.2.222 netmask 255.255.255.0 up route add default gw 192.168.2.1开机自启 修改网络设置文件 /etc/init.d/S40network #!/bin/sh # # Start the network.... ## Debian ifupdown needs the /run/network lock directory mkdir …

Vue3实现简单的瀑布流效果,可抽离成组件直接使用

先来看下效果图&#xff1a; 瀑布流中的内容可进行自定义&#xff0c;这里的示例图是通过不同背景颜色的展示进行区分&#xff0c;每个瀑布流中添加了自定义图片和文字描述。 实现方式&#xff1a; 1.建立子组件&#xff08;可单独抽离&#xff09;写出瀑布流的样式 文件名为…

C++-逻辑语句

if语句 基本格式&#xff1a; 只有判断结果为true&#xff0c;才会执行后续{}内的代码 if (要执行的判断&#xff0c;结果需是bool型) {判断结果true&#xff0c;才会执行的代码; }if (条件判断) { 如果判断结果为true&#xff0c;会执行的代码; }else{如果判断结果为false…

【工具】AFL+Unicorn|二进制程序模糊测试工具 AFL 和 Unicorn 的前世今生、安装以及 Python 使用实例

文章目录 【工具】AFLUnicorn&#xff5c;二进制程序模糊测试基础工具&#xff08;AFLUnicorn&#xff09;写在最前1. 系统环境2. 软件版本3. 背景知识3.1 AFL vs AFLplusplus3.2 QEMU vs Unicorn3.3 Unicorn vs UnicornAFL 4. 工具安装4.1 Ubuntu184.2 Ubuntu 20~224.3 收尾 5…

分布式事务——9种解决方案的原理与分类

目录 一、概要1. 分布式事务的概念2. 分布式事务解决方案分类 二、常见的分布式事务解决方案1. 基础的 2PC&#xff08;二阶段提交&#xff09;1.1 核心思想1.2 简介1.3 主要特点1.3.1 优点1.3.2 缺点 2. 基础的 3PC&#xff08;三阶段提交&#xff09;2.1 核心思想2.2 简介2.3…