线程安全问题(一)——锁的简单使用

多线程安全问题

  • 线程安全问题的引入
  • 案例引入
    • 多线程指令排序问题
  • 线程不安全的原因
  • 解决线程不安全的方法
  • 锁的引入
    • 上锁和解锁过程
    • 一个简单的锁Demo
    • 对这个案例进行几次修改
  • 总结

线程安全问题的引入

在前面的博文中,我们了解到通过Thread.join()的方法让线程进入等待,能够在一定程度上解决线程抢占式执行的问题。回忆点这里
那么由于多线程代码而导致的bug,这样的问题就是线程安全问题。

案例引入

在下面的代码中,我希望执行之后得到count=100000的结果。

private static long count;
    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();
        System.out.println("count:"+count);
    }

结果如图:通过结果,我们每次运行得到的答案都是不一样的。
在这里插入图片描述

多线程指令排序问题

在上面的demo中,执行count++的指令并不只有一条这么简单。它可以分为三个步骤:(1) load操作: 读取内存中count的数值,保存到cpu寄存器中。 (2) add操作: 将寄存器中的count+1 (3) save操作:将寄存器中count的数值存放回内存中。
看似三条指令的简单操作,在多线程并发执行中却容易导致许多问题。
在多线程随即调度的执行状况下,两个线程的指令执行相对顺序可能就会存在多种可能,下面我列出几种可能性。
在这里插入图片描述
在上面的顺序中,我们可以理解为 在第一次t1线程的count++之后,count=1的值存放于寄存器中,接下来t2线程count++的时候,load指令下读取的数值为count=0,之后save操作后count=1,最后执行t1线程的save后值不变。因此执行了两次count++操作后,count的值只加了一次,出现了覆盖现象
我们可以不断扩大到其他状况,如t1线程辛辛苦苦加了100次,但是t2线程最终存放count=1将值覆盖了。

线程不安全的原因

  • 线程在系统中是随即调度,抢占式执行的。
  • 多个线程同时修改同一个变量(参考上述例子)
  • 线程对变量进行修改操作,非原子指令
  • 内存可见性问题
  • 指令重排序问题

解决线程不安全的方法

对于原因2,我们可以通过join等方式防止这种情况的发生,但这样做并不普适,属于少有的情况。

锁的引入

对于案例中的count++操作,我们清楚它不是一个原子操作,因此,程序猿想出来了一个办法:将上述的一系列“非原子”操作打包成一个“原子”操作。这样就能够避免线程不安全的问题。
基于这样的背景,锁被创建出来了。

上锁和解锁过程

假设存在两个线程t1和t2,
(1)上锁:我们首先给t1加上锁(lock),t2也尝试加同一把锁,那么这时候t2线程就会阻塞等待,在Java中该线程处于Blocked状态。
(2)解锁:当线程t1执行完锁住的部分后,线程t1解锁,接着由线程t2通过锁竞争拿到该锁(lock),加锁成功,t2线程转变为Runnable状态。
通过锁的存在,使得线程之间存在互斥的关系。在两个线程之间尚且都要通过锁竞争,而存在多个线程的情况下自然也要通过竞争的方式占据锁。这里必须要明确一个条件:线程之间竞争的必须要是同一把锁

一个简单的锁Demo

通过下面的代码块进行简单的解释。

  • 首先new一个Object类的对象object1.我们要把这个对象作为锁。看到这里我们就可以清楚,锁不必是某种特定的类,他只是一个标识,只是一个对象即可。
  • 在这段案例中,存在main、t1、t2三个线程,main线程十分简单,通过Thread.join()的方法等待t1和t2两个线程运行结束之后打印出结果即可。
  • 在线程t1和t2中,在for循环中,我们可以看到synchronized (object1)。其中synchronized就是锁的关键字。在t1和t2都使用了这段语句,即在进行count++操作之前,需要进行锁竞争,只有拥有锁的一方才可以进行count++操作。
  • 在这段代码块中,t1和t2在for循环的时候是并行执行的,而在锁竞争的时候是串行执行的。这样计算下来比单线程所花费的时间要少许多。
private static int count;
public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
        Thread t1 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    synchronized (object1) {
                        count++;
                    }
                }
        });
        Thread t2 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    synchronized (object1) {
                    count++;
                    }
                }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);
    }

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

对这个案例进行几次修改

(1)接下来我们可以考虑,当锁对象为两个:object1和object2时,两个线程分别竞争两个不一样的锁,会出现什么情况
在这里插入图片描述
结果如下:
在这里插入图片描述
通过这个改变,我们可以理解不同的锁对象之间不存在互斥关系,因此二者之间也就不会发生锁竞争。

(2)将synchronized放在for循环外面的情况
在这种条件下,意味着当t1或t2某一个线程拿到这把锁之后,只有等循环结束以后才能释放了,很明显这样的情况所花费的资源甚至多于单线程。

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

(3)在下面的代码中,设计Counter类进行add和get操作,在上面的代码中,我们已经知道锁对象只是一个标识,不关心它是怎样的存在,因此在这里,我们大胆的把counter对象作为锁对象。

class Counter {
    public int count;

    public  void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+counter.getCount());
    }
}

运行结果如下图:
在这里插入图片描述
(4)如果我们把锁加到add()方法中,我们通过this来指代对应的对象,这样做的情况是当多个线程调用该方法的时候,如果使用的是同一个对象会进行竞争,如果是不同对象的话则不会进行竞争。
同含义的写法为:synchronized public void add()

class Counter {
    public int count;

     public void add() {
     	synchronizedthis){
        	count++;
        }
    }

    public int getCount() {
        return count;
    }
}

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

(5)对静态方法加锁
与(4)不同的是,加锁的对象为Counter这个类对象,因此如果多个线程调用func方法,则这些线程之间都会进行锁竞争

//第一种写法
public static void func(){
  synchronized (Counter.class) {
    //func
    }
}
//第二种写法
synchronized public static void func(){
  //func
}
   

总结

对于锁的概念需要逐渐深入,在本文中讲解了锁引入的原因以及锁的几种写法。
本文中使用的源码请戳此处

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

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

相关文章

达梦(DM8)数据库备份与还原(逻辑备份)二

一、达梦数据库的逻辑备份分四种级别的导出&#xff08;dexp&#xff09;与导入&#xff08;dimp&#xff09;的备份 第一种是&#xff1a;数据库级&#xff1a;导出或导入数据库中所有的对象。主要参数是&#xff1a;FULL 第二种是&#xff1a;用户级别&#xff1a;导出或导…

Linux系统中根下的目录结构介绍

一、Linux的路径分隔符 Linux系统中使用正斜杠(/)作为路径分隔符&#xff1b;每个目录的后面都默认带有一个正斜杠&#xff08;如&#xff1a;需要进入opt目录可以分别使用【cd /opt】或【cd /opt/】&#xff09; 二、Linux根目录下各个目录结构介绍 红色标识的文件夹为Linux的…

01数字电子技术基础

第一节课&#xff1a;introduction 导论 决定了这门课的学习方法、学习内容、一个大概的把握、虽不是具体的技术&#xff0c;不是细节&#xff0c;但是这是一节思想 每门课都重要&#xff0c;但侧重点不同。 学习前人的思想和营养&#xff0c;为自己所用。 1.课程性质&#x…

基于javassm实现的大学图书管理系统网站

开发语言&#xff1a;Java 框架&#xff1a;ssm 操作系统&#xff1a;Windows 7&#xff1b; 数据库&#xff1a;Mysql&#xff1b; 开发工具包&#xff1a;JDK Version1.6&#xff1b; JSP服务器&#xff1a;Tomcat&#xff1b; 浏览器&#xff1a;IE8.0&#xff0c;推荐…

使用Python和NLTK进行NLP分析的高级指南

在本文中&#xff0c;将利用数据集来比较和分析自然语言。 本文涵盖的基本构建块是&#xff1a; WordNet和同义词集相似度比较树和树岸命名实体识别 WordNet和同义词集 WordNet是NLTK中的大型词汇数据库语料库。WordNet维护与名词&#xff0c;动词&#xff0c;形容词&#…

关于ip地址的网页无法访问navigator的gpu、媒体、蓝牙等设备的解决方法

在使用threejs的WebGPURenderer渲染器时&#xff0c;发现localhost以及127.0.0.1才能访问到navigator.gpu&#xff0c;直接使用ip会变成undefined,原因是为了用户的隐私安全&#xff0c;只能在安全的上下文中使用&#xff0c;非安全的上下文就会是undefined&#xff0c;安全上下…

[面试题]Zookeeper

[面试题]Java【基础】[面试题]Java【虚拟机】[面试题]Java【并发】[面试题]Java【集合】[面试题]MySQL[面试题]Maven[面试题]Spring Boot[面试题]Spring Cloud[面试题]Spring MVC[面试题]Spring[面试题]MyBatis[面试题]Nginx[面试题]缓存[面试题]Redis[面试题]消息队列[面试题]…

第4章 客户端-客户端通信协议

Redis是用单线程来处理多个客户端的访问&#xff0c;因此作为Redis的开发和运维人员需要了解Redis服务端和客户端的通信协议&#xff0c;以及主流编程语言的Redis客户端使用方法&#xff0c;同时还需要了解客户端管理的相应API以及开发运维中可能遇到的问题。 几乎所有的主流编…

网络协议TCP/IP, HTTP/HTTPS介绍

TCP/IP协议 TCP/IP是一种基于连接的通信协议&#xff0c;它是互联网的基础协议。TCP代表传输控制协议&#xff0c;IP代表Internet协议。虽然这两个协议通常一起提及&#xff0c;但它们实际上是分开的&#xff1a;IP负责在网络中从一台计算机向另一台计算机发送数据包&#xff0…

【干货】Jupyter Lab操作文档

Jupyter Lab操作文档1. 使用须知2. 定制化Jupyter设置主题显示代码行数设置语言更多设置 3. 认识Jupyter界面4. 初用Jupyter运行调试格式化查看源码 5. 使用Jupyter Terminal6. 使用Jupyter Markdown7. 上传下载文件&#xff08;云服务器中的Jupyter Lab&#xff09;上传文件到…

【操作系统】信号处理与阻塞函数|时序竞态问题

&#x1f525;博客主页&#xff1a; 我要成为C领域大神&#x1f3a5;系列专栏&#xff1a;【C核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 ​ 关于阻塞函数和…

Go 语言学习笔记之通道 Channel

Go 语言学习笔记之通道 Channel 大家好&#xff0c;我是码农先森。 概念 Go 语言中的通道&#xff08;channel&#xff09;是用来在 Go 协程之间传递数据的一种通信机制。 通道可以避免多个协程直接共享内存&#xff0c;避免数据竞争和锁的使用&#xff0c;从而简化了并发程…

Edge 浏览器退出后,后台占用问题

Edge 浏览器退出后&#xff0c;后台占用问题 环境 windows 11 Microsoft Edge版本 126.0.2592.68 (正式版本) (64 位)详情 在关闭Edge软件后&#xff0c;查看后台&#xff0c;还占用很多系统资源。实在不明白&#xff0c;关了浏览器还不能全关了&#xff0c;微软也学流氓了。…

T-Reqs:一款基于语法的HTTP漏洞挖掘工具

关于T-Reqs T-Reqs全称为Two Requests&#xff0c;T-Reqs是一款基于语法的HTTP模糊测试漏洞挖掘工具&#xff0c;该工具可以通过发送版本为1.1或更早版本的变异HTTP请求来对目标HTTP服务器进行模糊测试以及漏洞挖掘。该工具主要通过下列三大步骤实现其功能&#xff1a;&#x…

冶金工业5G智能工厂工业物联数字孪生平台,推进制造业数字化转型

冶金工业5G智能工厂工业物联数字孪生平台&#xff0c;推进制造业数字化转型。传统生产方式难以满足现代冶金工业的发展需求&#xff0c;数字化转型成为必然趋势。通过引入5G、工业物联网和数字孪生等先进技术&#xff0c;冶金工业可以实现生产过程智能化、高效化和绿色化&#…

轻松掌握:工科生如何高效阅读国际期刊和撰写论文(下)

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三连支…

LeetCode 算法:将有序数组转换为二叉搜索树 c++

原题链接&#x1f517;&#xff1a;将有序数组转换为二叉搜索树 难度&#xff1a;简单⭐️ 题目 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡 二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9]…

visual studio打包QT工程发布exe安装包

一、实验环境 软件版本下载链接visual studioMicrosoft Visual Studio Community 2022 (64 位) - Current 版本 17.7.5QTv6.6.3NSISv3.10官网 或 百度云1234Windows11 二、程序准备 1、程序生成 使用 visual studio 打开工程&#xff0c;选择 Release 模式后&#xff0c;点…

韩顺平0基础学java——第31天

p612-637 IO流 IO流原理及流的分类 Java lO流原理 1.I/O是Input/Output的缩弓&#xff0c;IV/O技术是非常实用的技术&#xff0c;用于处理数据传输。 如读/写文件&#xff0c;网络通讯等。 2. Java程序中&#xff0c;对于数据的输入/输出操作以”流(stream)”的方式进行。 3…

系统漏洞复现与勒索病毒

知识点&#xff1a;SMB漏洞介绍、漏洞复现流程、勒索病毒攻击与防护 渗透测试相关&#xff1a; 基本概念&#xff1a; 渗透测试就是利用我们所掌握的渗透知识&#xff0c;对网站进行一步一步的渗透&#xff0c;发现其中存在的漏洞和隐藏的风险&#xff0c;然后撰写一篇测试报…