Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized

news2024/11/8 6:34:02

这里是Themberfue 

· 在上一节的最后,我们讨论两个线程同时对一个变量累加所产生的现象

· 在这一节中,我们将更加详细地解释这个现象背后发生的原因以及该如何解决这样类似的现象


线程安全问题

public class Demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        // "线程安全问题"

        // 会照成 "线程不安全问题"
        // t1.start();
        // t2.start();
        // t1.join();
        // t2.join();
        
        // 改为串行执行
        t1.start();
        t1.join();

        t2.start();
        t2.join();

        System.out.println(count);
    }
}

· 我们先回顾上述代码,如果两个线程并发执行逻辑,同时累加 count 变量 100,000 次后,得到的结果是一个随机值,且这个随机值一定小于 100,000

· 如果改为串行执行,就是 t1 执行完后,t2 再度执行,那么 count 的结果就为 100,000

· 为什么会产生这样的现象?

· 我们先从一行代码入手:count++,有的人就会问:这有什么好分析的,这不就是一个count + 1的操作吗?没错,的确是这样

· 众所周知:CPU(中央处理器)执行的是一系列指令,这些指令定义了它需要执行的逻辑操作,这些指令的集合统称为指令集,指令集有两种,一种是..... (再讲就串台了,这是计算机组成原理的知识哦)

· 常见的指令就有逻辑指令,算术指令等,那么,一个 count++ 其实分为三个指令操作,因为它还设计到变量的修改,而不是单纯地加法

· 我们都知道,把大象放进冰箱分三步:把把冰箱门打开,把大象装进去,再把冰箱门关上

· count++ 也分为三步操作:

        1. load:把内存中 count 的值,加载到 cpu 寄存器

        2. add:把寄存器中的 count 的值 + 1

        3. save:把寄存器中的 count 的值保存到内存中

PS:寄存器就是CPU处理日常任务的小工具,用来存放临时信息

· 操作系统对线程的调度的是随机的所以在执行这三条指令时,可能不是一口气全部执行完毕,而是执行了一半就不执行了,而后又执行了

· 比如执行 指令1 ,后被调度走,调度回来后执行 指令2 指令3

· 比如执行 指令1 指令2 ,后被调度走,调度回来后执行 指令3

· 比如执行 指令1 ,后被调度走,调度回来后执行 指令 2 ,后被调度走,调度回来后执行指令3

· 多线程的随机调度是造成这个bug出现的原因

· 上述为简单模拟了一遍两次 count++ 的大概流程

· 这是最为理想的情况,就是三条指令一次性执行完毕后再去执行下三条指令,但实际情况却不能保证每次发生这种理想的情况

 

· 上述情况才是经常发生的,也是导致bug的主要原因

· 尽管执行了两次 count++ 操作,但内存中保存的值为1,结果只增值了一次

· 产生上述问题的原因就是线程安全问题

· 根本原因就是操作系统对于线程的调度是随机的,也就是抢占式执行(这个策略在最初诞生多线程任务操作系统时就诞生了,是非常伟大的发明,后世的操作系统,都是这个策略)

· 第二个原因就是多个线程修改同一个变量

        如果是一个线程修改一个变量,不会产生上述问题

        如果是多个线程修改不同变量,同样的

        如果是多个线程不是同时修改同一个变量,同样的

        如果是多个线程同时读取一个变量,同样的

· 第三个原因就是修改的操作,不是原子的,如果是  count++ 是一条指令就可以执行完毕,那么认为该操作就是原子的

· 内存可见性问题

· 指令重排序

· 后续再讨论其细节


加锁

 · Java中解决线程安全问题的最主要的方案就是给代码块加锁,通过加锁,可以让不是原子的操作,打包成一个原子的操作

· 计算机中的锁操作,和生活中的加锁区别不大,都是互斥,排他。例如:你上厕所,对当前这个厕所间加锁,那么别人就不能进这个厕所间了,你出厕所门时,此时就是解锁

· 通过使用锁,对先前的 count++ 操作就可以将其变为原子的,在加上锁后,count++ 的三个指令就会完整执行完毕后才可能被调度走

· 加锁操作,不是讲线程锁死到CPU上,禁止这个线程被调度走,是禁止其他线程重新加这个锁,避免其他线程的操作,在这个线程执行过程中插队

· 加锁和解锁这个操作本身是操作系统提供的 api,但是很多语言都对其单独进行了封装,大多数的封装风格都是采取这两个函数:

Object.lock();

    // 执行的代码逻辑

Object.unlock();

· 但是这样写的弊端也很大,不能保证每次加上锁后都会记住去解锁,所以 Java 提供了一种更为简洁的方式去给某个代码块上锁:

synchronized {

    // 执行的代码逻辑

}

· 只要进入了代码块(进入 '{' 后)就会加上锁,只要出了代码块(出去 '}' 后)就会自动解锁

· 但在上述伪代码中,synchronized 的使用并不正确,单纯地加锁,但是此时另一个线程又要加锁,我们要怎么判断这个锁有没有被使用(锁又不止一个)

· 所以应该这样使用:

synchronized (Object) {

    // 执行的代码逻辑

}

· 没错,括号里填写的就是用来加锁的对象,这个对象一般称为锁对象,作为锁的作用去使用

· Object 表示一个类,Java 的所有对象都可以作为锁对象

Object locker = new Object();

synchronized (locker) {
    count++;
}
public class Demo16 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // Java中,任何一个对象都可以作为锁
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 对count的++操作进行上锁
                // load,add,save操作执行完才会调度走
                synchronized (locker) {
                    count++;
                }
            }
        });

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

        // 只有两个线程针对一个对象加锁,才会产生互斥效果
        // 一个线程被上了锁,另一个线程得阻塞等待,直到第一个线程解锁

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

· 单独对一个线程上一个锁,是不能发挥锁的作用的

· 只有两个线程针对一个对象加锁,才会产生互斥效果

· 一个线程被上了锁,另一个线程就得阻塞等待,直到那个线程解锁,才会继续向下执行

· 运行上述代码,count 的结果恒为 100,000,不可能出现其他值,也就解决了该代码逻辑的线程安全问题

· 通过加锁操作,count++ 操作的三个指令相当于合并成了一个指令,保证每个线程从内存中获取到的值是正确的

· 并不是加上了 synchronized 就一定保证线程安全,得要正确地使用锁,在该使用的时候使用锁,在对的地方使用锁

· 比如,在这个案例中,不是对 count++ 操作操作,而是在 for 循环开始前就上锁:

synchronized (locker) {
    for (int i = 0; i < 50000; i++) {
        count++;
    }
}

· 这样虽然也是上了锁,但是没什么意义,就相当于等到 for 循环逻辑全部结束后,再解锁,另一个线程停止等待,拿到锁

· 因为这两个线程就这一个相同逻辑,所以这么写就相当于变成了串行执行,不是并发了

· 采取 synchronized 的加锁方式,就可以确保一定会释放锁,不会遇到加锁后但是没有解锁的情况

· 除此之外,synchronized 还可以修饰方法,对这个方法加锁

class Counter {
    private int count;

    // 使用 synchronized 对方法进行上锁,就相当于是针对this上锁
    synchronized public void addCount() {
        // synchronized (this) {
            this.count++;
        // }
    }

    // 使用 synchronized 对静态方法进行上锁,就相当于是针对类对象上锁(反射)
    public synchronized static void func () throws ClassNotFoundException {
        synchronized (Class.forName("Counter")) {
            System.out.println("func");
        }
    }
    public synchronized static void fuc () {
        synchronized (Counter.class) {
            System.out.println("fuc");
        }
    }

    public int getCount() {
        return count;
    }
}

public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.addCount();
            }
        });

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

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + counter.getCount());
    }
}

· 我们如果查看 StringBuffer 类的方法,也可以看到类似的操作


· 下一节我们会更加深入多线程,了解到死锁等相关概念

· 毕竟不知后事如何,且听下回分解~~

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

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

相关文章

【taro react】 ---- 常用自定义 React Hooks 的实现【六】之类渐入动画效果的轮播

1. 效果 2. 场景 css 效果实现:可以看到效果图中就是一个图片从小到大的切换动画效果,这个效果很简单,使用 css 的 transform 的 scale 来实现图片的从小到大的效果,切换就更加简单了,不管是 opacity 还是 visibility 都可以实现图片的隐藏和显示的切换。React.Children.m…

杨辉三角,洗牌算法

杨辉三角 给定一个非负整数numRows&#xff0c;生成杨辉三角的前numRows行。 在杨辉三角中&#xff0c;每个数是它的左上方和右上方的数的和。 public List<List<Integer>> generate(int numRows){List<List<Integer>> ret new ArrayList<>();…

ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic

设计题目&#xff1a;海鲜自助餐厅系统的设计与实现 摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所…

ENSP ISOLATE隔离区域

如果用户想进行二层隔离&#xff0c;用户可以将不同的端口加入不同的VLAN&#xff0c;但这样会浪费有限的VLAN资源。采用端口隔离功能&#xff0c;可以实现同一VLAN内端口之间的隔离。用户只需要将端口加入到隔离组中&#xff0c;就可以实现隔离组内端口之间二层数据的隔离。端…

自攻螺钉的世纪演变:探索关键设计与应用

自攻螺钉作为现代工业和建筑中的不可或缺的标准部件&#xff0c;经过了超过100年的发展和创新。从1914年最早的铁螺钉设计到今天的自钻自攻螺钉&#xff0c;自攻螺钉的设计不断优化&#xff0c;以适应更复杂的应用需求。本文将回顾自攻螺钉的演变历程&#xff0c;分析其设计原理…

KTHREAD结构-->ApcState

1. ApcListHead[2] 2. KernelApcInProgress

深入浅出 Spring Boot 与 Shiro:构建安全认证与权限管理框架

一、Shiro框架概念 &#xff08;一&#xff09;Shiro框架概念 1.概念&#xff1a; Shiro是apache旗下一个开源安全框架&#xff0c;它对软件系统中的安全认证相关功能进行了封装&#xff0c;实现了用户身份认证&#xff0c;权限授权、加密、会话管理等功能&#xff0c;组成一…

魅力标签云,奇幻词云图 —— 数据可视化新境界

目录 目的原理详解建议 标签云&#xff1a;用于汇总生成的标签&#xff0c;一般是独立词汇运行前的准备代码示例 词云&#xff1a;对本文中出现频率较高的词&#xff0c;视觉上突出显示总结 目的 掌握文本与文档可视化&#xff1a;使用特定软件或编程语言&#xff08;如Python…

正则表达式在Kotlin中的应用:提取图片链接

在现代的Web开发中&#xff0c;经常需要从网页内容中提取特定的数据&#xff0c;例如图片链接。Kotlin作为一种现代的编程语言&#xff0c;提供了强大的网络请求和文本处理能力。本文将介绍如何使用Kotlin结合正则表达式来提取网页中的图片链接。 正则表达式基础 正则表达式是…

鉴源实验室·加密技术在汽车系统中的应用

随着汽车技术的快速发展&#xff0c;现代汽车已经不再是简单的交通工具&#xff0c;而是融合了多种智能功能的移动终端。无论是自动驾驶、车联网&#xff08;V2X&#xff09;&#xff0c;还是车内娱乐系统&#xff0c;数据传输和存储已经成为汽车生态系统中的关键环节。然而&am…

UE5.1 控制台设置帧率

仅个人记录&#xff0c;未经过严格验证。 也可通过控制台命令蓝图节点&#xff0c;在运行时执行 锁帧&#xff1a; 0->120帧 1-》60帧

SpringCloud Sentinel 服务治理详解

雪崩问题 微服务调用链路中的某个服务故障&#xff0c;引起整个链路中的所有微服务都不可用&#xff0c;这就是雪崩。 雪崩问题产生的原因&#xff1a; 微服务相互调用&#xff0c;服务提供者出现故障或阻塞。服务调用者没有做好异常处理&#xff0c;导致自身故障。调用链中的…

前端基础-html-注册界面

&#xff08;200粉啦&#xff0c;感谢大家的关注~ 一起加油吧~&#xff09; 浅浅分享下作业&#xff0c;大佬轻喷~ 网页最终效果&#xff1a; 详细代码&#xff1a; ​ <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…

《TCP/IP网络编程》学习笔记 | Chapter 4:基于TCP的服务器端/客户端(2)

《TCP/IP网络编程》学习笔记 | Chapter 4&#xff1a;基于TCP的服务器端/客户端&#xff08;2&#xff09; 《TCP/IP网络编程》学习笔记 | Chapter 4&#xff1a;基于TCP的服务器端/客户端&#xff08;2&#xff09;回声客户端的完美实现回声客户端的问题回声客户端问题的解决方…

使用 FFmpeg 进行音视频转换的相关命令行参数解释

FFmpeg 是一个强大的多媒体框架&#xff0c;能够解码、编码、转码、录制、播放以及流化几乎所有类型的音频和视频。它广泛应用于音视频处理任务中&#xff0c;包括格式转换、剪辑、合并、水印添加等。本文中简鹿办公将介绍如何使用 FFmpeg 进行一些常见的音视频转换任务。 安装…

ctfshow(316)--XSS漏洞--反射性XSS

Web316 进入界面&#xff1a; 审计 显示是关于反射性XSS的题目。 思路 首先想到利用XSS平台解题&#xff0c;看其他师傅的wp提示flag是在cookie中。 当前页面的cookie是flagyou%20are%20not%20admin%20no%20flag。 但是这里我使用XSS平台&#xff0c;显示的cookie还是这样…

从0开始学习Linux——网络配置

往期目录&#xff1a; 从0开始学习Linux——简介&安装 从0开始学习Linux——搭建属于自己的Linux虚拟机 从0开始学习Linux——文本编辑器 从0开始学习Linux——Yum工具 从0开始学习Linux——远程连接工具 从0开始学习Linux——文件目录 上一个教程中&#xff0c;我们了解了…

python在word中插入图片

本文讲解python如何在word文档中插入图片&#xff0c;以及指定插入图片的段落。 1、在新建的word文档中插入图片 import win32com.client as win32 from win32com.client import constants # 1&#xff09;打开word应用程序 doc_app win32.gencache.EnsureDispatch(Word.App…

亚信安全新一代WAF:抵御勒索攻击的坚固防线

近年来&#xff0c;勒索攻击已成为黑客的主要攻击手段。新型勒索攻击事件层出不穷&#xff0c;勒索攻击形势愈发严峻&#xff0c;已经对全球制造、金融、能源、医疗、政府组织等关键领域造成严重危害。如今&#xff0c;勒索攻击手段日趋成熟、攻击目标愈发明确&#xff0c;模式…

Linux qt下是使用搜狗輸入發

1.下载一个编译好的包 https://github.com/sixsixQAQ/fcitx5-qt 出处&#xff1a;这里 2.根据QT5&#xff0c;或者QT6选择下载 3.使用 把那个libfcitx5platforminputcontextplugin.so放到下面的路径&#xff1a; <你的Qt安装目录>/gcc_64/plugins/platforminputcontex…