线程安全

文章目录

  • 观察线程安全问题
  • 线程安全的概念
  • 出现线程安全问题的原因
    • 共享数据
    • 原子性
    • 总结
  • synchronized - 锁
    • synchronized 特性
      • 互斥
      • 可重入
    • synchronized 的使用
      • 修饰普通方法
      • 修饰静态方法
      • 修饰代码块
  • 解决线程安全问题
    • 两个线程两把锁
    • 哲学家就餐问题 - N个线程M把锁
    • 解决策略
  • 死锁成因总结

观察线程安全问题

有些代码在单线程环境下执行,完全正确,但是如果同样的代码,在多线程环境下执行,就可能出现Bug,这种问题称为“线程安全问题” 或是"线程不安全"。我们先看一个例子。

public class Demo01_CountIncrease {
    private static int count = 0;
    private static Object locker = new Object();

    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++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();

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

        System.out.println(count);
    }
}

运行结果~~

67779

按照我们对这个多线程程序的预期,一个线程对count变量自增5w次,结果应该是10w,但是这里的结果不是,这里明显是bug,这个bug是由多线程引起的,这就是线程安全问题。那么为什么会产生这样的问题呢?这就要对线程安全有一定的了解.

线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:如果单线程环境下代码运行的结果是符合我们预期的,多线程环境结果也符合预期,则说这个程序是线程安全。反之则是线程不安全的,即出现了线程安全问题。

出现线程安全问题的原因

在这里插入图片描述
综上,我们意识到了,多线程程序要保证无论操作系统调度顺序如何,我们都要保证写出的程序能够正常执行.如果只是如上图这样调度的话并不会出现线程安全问题

共享数据

上面的代码中count,是一个静态成员变量,由整个类共享。此时多个线程都可以访问到这个内存。从而对这个内存进行一些非原子操作,导致了线程安全问题。如果t1,t2线程操作的是两个变量就不会产生这个问题,因为无论怎么调度,自增的结果是独立保存的.

原子性

指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。原子性的操作是一个不可分割的操作。一条java 语句不一定是原子的,也不一定只是一条指令,上面代码中的count++其实是由三条指令共同完成。

  • load 将count的值从内存中读到寄存器中
  • add 将寄存器中count的值进行+1
  • save 将寄存器的值写回内存
    在这里插入图片描述
    在这里插入图片描述

从上图的解释中,我们可以清晰的看到,两个线程的两次自增只生效了一次,如果t1线程load后t2线程对count自增多次,那么t2线程做的都是无用功,当t1线程再次被调度执行时,回覆盖掉原有的内存.这是产生线程安全的重要原因,++操作不是原子的.

非原子性是产生线程安全的一个重要原因,如果一个线程正在对一个变量操作,中途其他线程穿插执行,这个操作被打断了,结果就可能是错误的。

总结

总结一下产生线程安全的原因

  1. 操作系统中,线程调度执行的顺序是随机的,抢占式执行
  2. 两个线程针对同一个变量进行修改,即修改共享数据
    1. 一个线程,针对同一个变量进行修改 。 线程安全
    2. 两个线程针对两个不同的变量修改。 线程安全
    3. 像个线程针对一个变量进行读取。 线程安全
  3. 修改操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

synchronized - 锁

锁是为了解决线程安全问题引入的,对代码进行枷锁操作,可以让这部分代码不会被其他线程穿插执行(变为并行),这是锁的互斥特性,如果一个线程已经持有锁,再进行枷锁操作,一般来说就会产生死锁。

synchronized 特性

互斥

synchronized 会起到互斥效果, 如果t1线程已经对locker对象加锁成功,那么t2线程在想对locker对象进行加锁是,就会进入阻塞,这种情况就是产生所竞争了

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

可重入

Java中的synchronized 是一把可重入锁,一个线程可以对一个锁对象进行多次枷锁,而不会出现死锁的情况。而C++的std::mutex则是一把不可冲入锁,如果针对一个线程针对一个锁对象进行多次枷锁操作就会产生死锁的情况。

public class Test5 {
    private static Object locker = new Object();
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           synchronized (locker) { // ①
               synchronized (locker) { // ②
                   System.out.println("可重入锁验证");
               }// ③
           }// ④
        });
        t.start();
    }
}

执行结果~~


在上述代码中t线程针对locker加了两次锁,我们来分析一下。我们以不可重入锁分析当前代码,当代码执行到①时,此时第一次获取锁,继续执行到②时,此时再次获取锁,而由于锁已经被t1获取了,还没有释放,所以这里拿不到锁就不能继续往下执行了,就会产生死锁的情况。

但是Java中的synchronized是一把可重入锁,可以针对一个对象多次枷锁,对象头中有一个计数器进行记录当前线程针对锁枷锁的次数,当线程一次次释放锁将计数器归零时,才会真正的释放锁。

synchronized 的使用

对象在构造时,不仅构造了成员属性的空间,还开辟了一些其他的空间。比如对象头Class Header,对象头中包括mark word和class pointer,其中mark word就是记录锁的,而class pointer则是指向该对象所属的类。所以锁是存放在对象头中,枷锁是针对对象枷锁,所以在枷锁之前得有一个锁对象。

修饰普通方法

    // ①
    synchronized public void increase1() {
        count++;
    }
	// ②
     public void increase2() {
        synchronized (this) {
            count++;
        }
    }

这个代码中的①②是等价的。

修饰静态方法

// ③
synchronized public static void increase3() {

 }
// ④
public static void increase4() {
      synchronized (Counter.class) {

      }
}

这个代码中的③④是等价的。

修饰代码块

public class Test6 {
	private static Object locker = new Object();
    public  void test() {
    	synchronized(locker) {
		
		}
    }
}

解决线程安全问题

两个线程两把锁

public class Demo16 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    // 此处的sleep很重要,要确保 t1 和t2 分别拿到一把锁后在获取第二把锁
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t1 枷锁成功");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t1 枷锁成功");
                }
            }
        });

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

执行结果~~


这个代码中出现了两个线程两把锁,但是由于上锁的时机不对,形成了环就造成了死锁的情况,好比车钥匙在家里,家里的钥匙在车里~~

哲学家就餐问题 - N个线程M把锁

我们强化一下上面的命题,我们发现当出现N个线程M把锁,也可能会产生死锁。

哲学家就餐问题:

  • 五位哲学家围坐在一张圆形餐桌就餐(吃意大利面)
  • 他们可以做以下两件事情之一:吃面或思考。
  • 吃东西的时候,他们就停止思考,思考的时候也停止吃东西。
  • 餐桌中间有一大碗意大利面,每两个哲学家之间有一只筷子。
  • 因为用一只筷子很难吃到意大利面,所以假设哲学家必须用一双筷子吃面。
  • 他们只能使用自己左右手边的那两只筷子。

如果每个哲学家都拿起自己左手边的筷子,这时五个哲学家都只有一只筷子,从而大家都不能吃到面,这就产生了死锁。在这个过程中,哲学家是线程,意大利面好比“共享资源”,而想要拿到共享资源就得同时获取到两把锁。

那么这个问题怎么解决呢?我们给每根筷子编号,每个哲学家只能拿到自己左右手边较小编号的筷子。
哲学家就餐问题这样哲学家A拿到1号筷子,哲学家B拿到2号筷子,哲学家C那都3号筷子,哲学家D拿到4号筷子。当哲学家E想要拿1号筷子时,1号筷子已经被哲学家A拿到了,所以哲学家E就在这等着。此时哲学家D看见5号筷子空闲出来就拿起5号筷子开始炫面,哲学家D吃完放下两只筷子,哲学家C就可以开始吃面了,后面同理。这样所有的哲学家都能够吃到面。

解决策略

我们可以向解决哲学家问题那样,给锁编号约定从小的锁开始使用,这样就可以避免锁成环,从而避免了死锁问题。

在某些特定场景下可以通过调整代码结构,来规避线程安全问题。而解决线程安全的主要手段主要是对代码进行枷锁操作。

我们这里约定t1 t2线程都从编号小的锁开始使用,这样就可以解决这个问题了。

public class Demo16 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    // 此处的sleep很重要,要确保 t1 和t2 分别拿到一把锁后在获取第二把锁
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t1 枷锁成功");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t2 枷锁成功");
                }
            }
        });

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

执行结果~~

t1 枷锁成功
t2 枷锁成功

进程已结束,退出代码为 0

;我们约定好枷锁顺序后,t1获取到锁之后t2就要阻塞等待等到t1释放完锁1之后在进行获取锁,这样就不会形成环,从而解决了线程安全问题。

注:

  • 枷锁对象不重要,重要的是通过这个对象来区分两个对象是否在竞争同一个锁
  • 如果两个线程针对同一个对象枷锁,就会产生锁竞争,如果不是针对同一个对象枷锁,就不会有所竞争,仍然是并发执行

死锁成因总结

死锁要形成要满足四个冲要条件:

  1. 互斥使用(锁的基本特性): 当一个线程持有一把锁后,另一个线程也想获取当前锁,就会进入阻塞
  2. 不可抢占(锁的基本特性):当锁以及被一个线程获取后,另一个线程只能等之前加锁的线程解锁后,才能获取锁,不能强行抢占
  3. 请求保持(代码结构):一个线程尝试获取多把锁是,之前获取的锁并不会释放
  4. 循环等待(代码结构):等待的以来关系形成环

综上,我们只需在加锁时进行约定,按照一定的顺序进行加锁,避免加锁的依赖形成环,就可以破解死锁了.

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

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

相关文章

回归算法优化过程推导

假设存在一个数据集&#xff0c;包含工资、年龄及贷款额度三个维度的数据。我们需要根据这个数据集进行建模&#xff0c;从而在给定工资和年龄的情况下&#xff0c;实现对贷款额度的预测。其中&#xff0c;工资和年龄是模型构建时的两个特征&#xff0c;额度是模型输出的目标值…

【NLP】GPT 模型如何工作

介绍 2021 年&#xff0c;我使用 GPT 模型编写了最初的几行代码&#xff0c;那时我意识到文本生成已经达到了拐点。我要求 GPT-3 总结一份很长的文档&#xff0c;并尝试了几次提示。我可以看到结果比以前的模型先进得多&#xff0c;这让我对这项技术感到兴奋&#xff0c;并渴望…

Linux 磁盘/分区/修复 命令

目录 1. lsblk&#xff08;list block devices&#xff09; 2. fdisk&#xff08;fragment disk&#xff09; 3. gdisk 4. mkfs&#xff08;make filesystem&#xff09; 5. df&#xff08;display file-system disk space usage&#xff09; 6. du 7. fsck&#xff08;file-sy…

千帆Llama 2中文增强技术介绍--SFT,预训练,指令优化

目录 千帆Llama 2中文增强技术介绍 SFT&#xff0c;预训练&#xff0c;指令优化 千帆Llama 2中文增强技术介绍 SFT&#xff0c;预训练&#xff0c;指令优化

JavaScript中的继承

前言 继承 1.借用构造函数继承也叫经典继承 2.原型链继承 3.组合继承 1 2 1.经典继承 借用构造函数实现继承 // 创建父构造函数 function Animal(type,weight,age,length){this.type type;this.weight weight;this.age age;this.length length; }; Animal.prot…

一个工具让你明白“万丈高楼平地起”,拒绝重复造轮子!

大家在公司工作当中是不是很多时间装环境很麻烦&#xff0c;一个项目要上线了&#xff0c;开始网上搜了一边又一遍的环境搭建教程&#xff1f;等到下一个项目要上线了&#xff0c;又上网上搜了一边又一遍的环境搭建教程。关键天花乱坠的互联网&#xff0c;找不到很靠谱的呀。有…

Python数据分析30w人都在看

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

深入了解Performance API:优化网页性能的利器

在现代Web开发中&#xff0c;优化网页性能是至关重要的。用户对于加载速度和交互性能的要求越来越高&#xff0c;而Performance API作为一组用于测量和监控网页性能的JavaScript接口&#xff0c;为开发者提供了丰富的工具和信息。本文将深入探讨Performance API的各个方面&…

2021年09月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 执行下图所示程序,舞台上的角色? A:在1秒内滑行到随机位置 B:不断地重复滑行到随机位置 C:只有按下空格键的时候,才会滑行到随机位置 D:只有按下空格键以外键的时候,才会滑行…

SpringMVC问题

文章目录 SpringMVC运行流程MVC的概念与请求在MVC中的执行路径&#xff0c;ResponsBody注解的用途SpringMVC启动流程 SpringMVC运行流程 • 客户端&#xff08;浏览器&#xff09;发送请求&#xff0c;直接请求到 DispatcherServlet 。 • DispatcherServlet 根据请求信息调用 …

vscode-insiders Remote-SSH XHR failed无法访问远程服务器

问题概述&#xff1a; destFolder/home/apple/.vscode-server-insiders > destFolder2/vscode-cli-05cd2640ec8a106a4ee99cb38e6ee34fbec04f11.tar.gz > 194f252f7426:trigger_server_download_end > Waiting for client to transfer server archive... > W…

C语言好好题(一维数组)

两天没有更新了&#xff0c;贴纸们&#xff0c;有没有想我呀。&#x1f604;&#x1f604;&#x1f604; 好了&#xff0c;就寒暄到这里吧&#xff0c;下面请看题&#xff1a; 有序序列判断 输入一个整数序列&#xff0c;判断是否是有序序列&#xff0c;有序&#xff0c;指序列…

Postman如何使用(一):导入导出和发送请求查看响应

一、Postman如何导入导出打包的应用 在Postman中导入导出我们的 测试数据包 和 工作环境 非常的方便&#xff1a; 导出数据包的方法如下&#xff1a; 如果你想学习自动化测试&#xff0c;我这边给你推荐一套视频&#xff0c;这个视频可以说是B站播放全网第一的自动化测试教程…

10年开发工程师总结,8大主流程序员兼职平台,月入30k不是梦!

今年互联网行业陆续裁员减薪&#xff0c;许多人怨声载道的同时也开始另谋出路。而对于程序员更是应该提早做好准备&#xff0c;活跃在兼职接单的最前沿。 我们程序员是一门技术工种&#xff0c;与互联网其他行业相比薪水会相对高一点&#xff0c;不过钱也不是那么好赚的&#…

2023-11-21 LeetCode每日一题(美化数组的最少删除数)

2023-11-21每日一题 一、题目编号 2216. 美化数组的最少删除数二、题目链接 点击跳转到题目位置 三、题目描述 给你一个下标从 0 开始的整数数组 nums &#xff0c;如果满足下述条件&#xff0c;则认为数组 nums 是一个 美丽数组 &#xff1a; nums.length 为偶数对所有满…

腾讯三季度财报解读:AI大模型成下个十年的新支点?

2023年&#xff0c;腾讯重回高增长轨道。 近日&#xff0c;腾讯披露了2023年第三季度财报&#xff0c;营收1546.25亿元&#xff0c;同比增长10%&#xff1b;非国际通用会计准则下的净利润为449.21亿元&#xff0c;同比增长39%。此前两个季度&#xff0c;腾讯的营收、净利润增速…

【西行纪年番】孙悟空对战阴界王,素衣奄奄一息,巨灵拳霸气一击

Hello,小伙伴们&#xff0c;我是拾荒君。 《西行纪年番》第20集已更新。为了救回素衣&#xff0c;孙悟空想尽办法&#xff0c;最后他拜托沙悟净帮忙&#xff0c;终于成功把自己传送到阴界。原来&#xff0c;素衣的魂魄被阴界王藏在了他制造的人偶之中。沙悟净提醒孙悟空必须在…

【LeetCode二叉树进阶题目】606,102,107

二叉树进阶题目 606. 根据二叉树创建字符串解题思路及实现 102. 二叉树的层序遍历解题思路及实现 107. 二叉树的层序遍历 II解题思路及实现 606. 根据二叉树创建字符串 描述 给你二叉树的根节点 root &#xff0c;请你采用前序遍历的方式&#xff0c;将二叉树转化为一个由括号…

一次解决套接字操作超时错误的过程

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> 在windows客户端使用QTcpSocket连接一个ubuntu服务端程序&#xff0c;出现套接字操作超时的错误。开始感觉还莫名其妙的&#xff0c;因为之前连接都是好好…

基于springboot实现“漫画之家”系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现“漫画之家”系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&am…
最新文章