编程(39)----------多线程中的锁

news2024/11/24 13:01:32

假设一个这样的场景: 在多线程的代码中, 需要在不同的线程中对同一个变量进行操作. 那此时就会出现问题: 多线程是并发进行的, 也就是说代码运行的时候, 俩个线程会同时对一个变量进行操作, 这样就会涉及到多线程的安全问题:

class Counter{
    public int count;

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

        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在这个代码中, 两个线程会分别对count进行自增五千次, 按理说最后打印的结果是一万. 但实际上,多次运行后代码的结果,很难做到一万, 常见于八九千的结果. 

其原因在于, add的过程并非不可拆分的, 也就是不具有原子性. 在实际的运行中, add可以大致分为三步: 读取, 加一, 最后再赋值. 当然这并非专业的术语说法, 这里只简单的以此为描述. 

由于两个线程同时进行, 也就是都要执行这三步, 且是以抢占式进行执行. 那执行顺序就必然乱套了. 很可能会出现线程1刚将count原值读入, 线程2就将其赋值走了, 根本没来得及加一. 这种还未执行完就将其读入的操作, 也可称其为脏读. 

                                                                                 

 为避免这种乱套的多线程安全问题, 常用办法便是采用加锁(Synchronized), 其用于修饰方法和代码块. 但是特别注意, 加锁是锁的对象. 当某个对象加锁后, 只有当其再解锁后, 另一个线程才能重新获取锁, 否者会陷入阻塞等待的状态:

                                                                                

 这样的操作就能保证在执行完一整个add后再执行下一个add. 虽会降低运行速率, 但能保证代码的准确性. 代码上的修改只需将add进行加锁即可保证得到准确的结果:

//只需在此处加锁即可
    public synchronized void add(){
            count++;
    }
}

//或者代码块加锁
    public void add(){
        synchronized (this) {
            count++;
        }
    }

  若两个线程针对不同对象加锁或者一个加锁一个不加锁, 那么也不会存在阻塞等待的情况.

还有一种特殊情况: 多重锁. 即一个线程加了两把锁, 虽然说当一个线程对对象上锁后, 另一个线程是应该阻塞等待的, 但此时若上锁线程就是要访问的线程呢? 这时是否可以考虑开绿灯呢? 这就好比小偷偷不属于自己的东西, 这是不被允许的犯罪行为. 那如果他偷的是自己的东西呢? 这完全是可以的, 因为这压根就不算偷窃.

因此, 对于可以实现多重锁的关键字, 就被认为是可重入的, 反之是不可重入. 在java中的synchronized是属于可重入, 也就是说, 加上述代码合并运行, 仍可以得到正确的结果, 但并非所有的锁都支持该功能:

 //可重入
    public synchronized void add(){
        synchronized (this) {
            count++;
        }
    }

若不支持可重入, 则会陷入死锁状态, 卡在那里 一直阻塞等待.

当然, 死锁的状态并非只有上述的这一种. 第二种是两个线程两把锁, 即两个线程先分别加锁, 然后再尝试获得对方的锁:

public class demo2 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    System.out.println("获取锁2");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized ((lock1)){
                    System.out.println("获取锁1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

在这个代码中就能够看出, 当两个线程将锁1 锁2获取后, 要相互获取对方的锁, 但对方的锁未解锁, 因此在这种情况想两个线程都被阻塞, 不能继续运行. 在这种情况下代码会一直处于运行状态. 可以用jconsole观察到线程是属于阻塞状态.

                                                                                   

 第三种死锁即第二种死锁的一般情况, 多线程多把锁而非两把锁. 这里涉及到一个经典的吃面问题. 假设, 有一个圆桌, 共坐了五个人, 每两个人之间, 放了一根筷子. 也就是说共放了五根筷子.

假设吃面的人必须得先拿起他左边的筷子, 再拿起他右边的一根筷子. 那在这种情况下考虑极端情况, 当五个人同时都想吃面时, 会同时都拿起左边的筷子, 且右边没有筷子可拿. 这个时候就僵住了, 谁也吃不了面, 谁也不会放下筷子. 同理, 在多线程种, 每个线程就好比每个人, 每跟筷子就好比每个锁, 考虑极端情况, 会出现这种全部僵在一起的状态.

要解决这个问题, 就得先了解死锁的必要条件:\

1. 互斥使用. 线程一上锁, 线程二只能等着.

2. 不可抢占. 线程一获得锁之后, 只能自己释放锁, 而不能由线程二强行获取锁

3.保持稳定性. 若线程一已经获得锁A, 它再尝试获得锁B时, 锁A是不会因为线程一获得锁B而解锁锁A.

4.循环等待. 也就是刚才所演示的. 线程一获得锁A的同时, 线程二获得锁B. 然后线程一要获得锁B, 线程二要获得锁A, 僵持不下.

对于Synchronized而言, 其实必要条件只有第四点. 前三点是无法去改变的. 但对于其他锁来说不一定. 因此, 想要解决死锁, 就只能从, 循环等待入手.

解决方法是, 给每一把锁标号, 再按照标号的一定顺序进行加锁.

                                                                                      

以吃面来举例. 将每根筷子标号, 并规定拿筷子必须从小号开始拿. 对应多线程种按锁的标号顺序由小到大加锁. 这样的话, 一号筷子和二号筷子之间的人就拿一号, 二号筷子和三号筷子之间的人就拿二号, 以此类推.

当轮到一号筷子和五号筷子之间的人拿筷子时, 出现问题了. 由于规定按小号拿, 因此应该是拿一号筷子而非五号筷子. 但此时的一号筷子已经被占用. 因此他只能等待, 也就是多线程中的阻塞. 与此同时, 前一个人可以再拿到四号筷子的基础上拿到五号筷子, 也就是获取到锁, 从而执行多线程. 以这种方式, 就不会出现所有人都吃不到面, 避免所有线程都处于阻塞状态. 反应到代码中, 就只需将锁调换一下即可:

public class demo2 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        //标号: 锁1 为一号, 锁2 为二号. 由小到大加锁
        Thread t1 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    System.out.println("获取锁2");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized ((lock2)){
                    System.out.println("获取锁1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

除此以外, 解决这类问题还可以使用银行家算法. 但是在实际工作中, 使用并不广泛. 因为其过于复杂, 实用性不高.

-------------------------------------------最后编辑于2023.6.1 下午两点左右

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

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

相关文章

RPC核心原理(整体架构/调用过程)

Server: Provider ,暴露服务,服务提供方 Client: Consumer ,服务消费,调用远程服务 Registry:服务注册与发现 RPC的调用过程如下&#xff1a; 第一步&#xff1a;server会将他需要暴露的服务以及他的地址信息注册到Registry这一注册中心。 第二步&#xff1a;client通过注册…

【VScode】ESLint :warning Delete `CR` prettier/prettier

一. ESLint 作用 检查 Javascript 编程时的语法错误。 新建或修改文件时报错 原因 Windows系统 &#xff0c;clone的代码会自动把换行符 LF转为回车符CRLF&#xff0c;这时本地的代码都是回车符。可在prettier.config.js中查看到 检查配置&#xff08;ESLint中是…

linux(system V标准)信号量

目录&#xff1a; 1.什么是信号量 2.信号量的本质 1.什么是信号量 2.信号量的本质 什么是临界资源呢?? 凡是倍多个执行流同时访问的资源就是临界资源&#xff01;&#xff01;&#xff01; 我们看一个问题&#xff0c;我们fork&#xff08;&#xff09;之后创建一个子进程&a…

redis----缓存穿透、击穿、雪崩问题解决

缓存 缓存更新方式 这是决定在使用缓存时就该考虑的问题。 设置缓存数据的TTL&#xff0c;当缓存数据失效后&#xff0c;如果有系统要请求&#xff0c;则会查询数据库并插入缓存&#xff08;被动更新&#xff09; 不友好在各类会往mysql写入数据的系统中&#xff0c;植入更新…

360天御滑块

又来水文章&#xff1f;当然&#xff0c;闲着无聊&#xff0c;走过路过&#xff0c;不要错过&#xff0c;点进来多看看。 两种类型。滑块&#xff0c;文字点选。此案例就以网友投稿的滑块来说。 怎么说呢&#xff0c;名字好听&#xff0c;本次难点&#xff0c;js混淆&#xff…

图文并茂教你快速入门React系列02-state

State 在React中&#xff0c;什么是state&#xff1f; 在 React 中&#xff0c;随时间变化的数据被称为状态&#xff08;state&#xff09;。你可以向任何组件添加状态&#xff0c;并按需进行更新。 如何添加使用state? 点击获取教程代码关于此框架–More docs 使用 useSt…

记一次Redis版本新特性导致的主从切换故障

背景 最近一组业务redis数据不断增长需要扩容内存&#xff0c;而扩容内存则需要重启云主机&#xff0c;在按计划扩容升级执行主从切换时意外发生了数据丢失与master进入只读状态的故障&#xff0c;这里记录分享一下。 业务redis高可用架构 该组业务redis使用的是一主一从&am…

基于卷积神经网络和连接性时序分类的语音识别系统,含核心Python工程源代码(深度学习)个人可二次开发

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 特征提取2. 声学模型3. CTC 解码4. 语言模型 系统测试工程源代码下载其它资料下载 前言 本项目基于卷积神经网络和连接性时序分类方法&#xff0c;采用中文语音数据集进行训练&#xff0c;实现声音转录为中文拼…

Java枚举中定义属性

文章目录 1、复习枚举2、自定义属性3、自定义属性枚举类和常量的对比4、常用方法5、枚举自定义属性在开发中的应用&#xff1a;字典表6、补充&#xff1a;入参校验 刚接触枚举时的例子太简单&#xff0c;就一个Season枚举类&#xff0c;里面四个常量值&#xff0c;后来开发中看…

接口幂等方案

文章目录 概要方案乐观锁数据库唯一索引令牌tokentoken通过另一个接口从服务端获取客户端自身生成token 总结 概要 所谓接口幂等性&#xff0c;就是一次和多次请求某一个资源对于资源本身应该具有同样的影响。接口幂等的应用很广&#xff0c;小到防止表单重复提交&#xff0c;…

使用kettle完成学生成绩登记需求

&#xff08;一&#xff09; 使用kettle完成学生成绩登记需求 学生成绩表下表所示。(自己创建一个学生表) 在MySQL中创建一个名为school的数据库&#xff0c;并在school数据库中创建一个名为score的表&#xff0c;使用Kettle将Excel形式的学生成绩表导入MySQL的score表 1&am…

机器视觉海康工业相机SDK参数设置获取

视觉人机器视觉培训-缺陷检测项目-食品行业草鸡蛋外观检测 相机参数类型可分为六类,除 command 参数外,每一类都有其对应的设置与获取函数接口。 表 1 参数类型及对应函数接口介绍 *详细函数接口可参考 SDK 手册: ​C:\Program Files (x86)\MVS\Development\Documentation…

【已解决】微信小程序报错:request 合法域名校验出错 如若已在管理后台更新域名配置,请刷新项目配置后重新编译项目,操作路径:“详情-域名信息”

【已解决】微信小程序报错&#xff1a;request 合法域名校验出错 如若已在管理后台更新域名配置&#xff0c;请刷新项目配置后重新编译项目&#xff0c;操作路径&#xff1a;“详情-域名信息” 场景复现解决方法 知识专栏专栏链接微信小程序专栏https://blog.csdn.net/xsl_hr/c…

工控设备如何防勒索病毒

目前现状 无论是中小企业还是大型企事业单位&#xff0c;均有属于自己的内网或公有云服务器。这些服务器有的是专门的SVN、GIT代码服务器&#xff0c;有的是文档存储服务器&#xff0c;有的是应用服务器。服务器是企业的核心命脉&#xff0c;所有知识产权及多年心血都集中汇总…

LeetCode——Pow(x, n)

一、题目 50. Pow(x, n) - 力扣&#xff08;Leetcode&#xff09; 实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xⁿ &#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000示例 2&#x…

Jetpack Compose中的状态栏适配(Window Insets)

除了app的内容区域外&#xff0c;还有一些其他的固定元素会显示在手机屏幕上&#xff0c;顶部的状态栏、 刘海、 底部的导航栏&#xff0c;还有输入法键盘&#xff0c;它们都是系统的UI&#xff0c; 也叫Insets. 如图所示: 顶部的状态栏通常被用来展示通知, 设备状态等; 底部导…

软考A计划-网络规划设计师-学习笔记-上

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

Linux 如何判断文件的类型

在Linux中&#xff0c;我们如何判断一个文件的类型和用户权限呢&#xff1f; 在c语言中&#xff0c;Linux为我们提供了一个结构体stat我们可以通过 #include<sys/stat.h>引入后使用。然后通过stat中的st_mode来判断文件的类型。如下图&#xff0c;我们要知道文件是什么类…

超级入门:R 语言的 5 种基本数据类型

一、R语言简介 R语言是一种用于统计计算和绘图的编程语言&#xff0c;它是由新西兰奥克兰大学的 Ross Ihaka 和 Robert Gentleman 开发的。R语言支持向量和矩阵计算&#xff0c;因此也可以用于数值分析和线性代数。它主要应用于数据分析、统计学习、数据挖掘、数据可视化等领域…

【Springboot】集成QQ邮箱信息发送

系列文章目录 文章目录 系列文章目录前言添加Maven依赖QQ邮箱开启POP服务配置application.properties文件Controller层编写 vue前端&#xff08;也可以直接省略&#xff09; 前言 这篇博客用于简单实现SpringBoot中发送请求&#xff0c;用户可以收到邮件。 添加Maven依赖 <…