《JavaSE-第二十二章》之线程安全问题

news2024/12/26 11:36:44

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


文章目录

  • 共享受限资源
    • 什么是线程安全问题?
      • 存在线程安全问题
        • 线程不安全的原因
      • 原子性
      • 内存可见性
      • 指令重排序
      • synchronized的特性
        • 1. 互斥
        • 2. 可重入
        • synchronized使用示例
          • 1. 直接修饰普通方法 锁的 Counter对象
          • 2. 修饰静态方法: 锁的 Counter类的对象
          • 3. 修饰代码块:明确指定锁哪个对象
      • volatile
        • volatile与synchronized

共享受限资源

在单线程序中,只有一个线程在干活。因此不会存在多个线程试图同时使用同一个资源。这就好比,不允许两个人在同一个停车位停车,两个人同时使用一个坑位,甚至是两个人坐在公交车上的同一个位置。并发虽然能同时做多个事情,但是,多个线程彼此可能互相干涉。如果无法避免这种冲突,就可能会发生两个线程同时修改同一个变量,两个线程同时修改同一个支付宝账户,改变同一个值等诸如此类的问题。

什么是线程安全问题?

操作系统中的线程调度采取的是抢占式执行,多个线程的调度执行过程,可以视为"随机的",而这些线程可能会同时运行某段代码。程序每次运行的结果和单线程运行的结果是一样的,而且其他的变量和预期的也是一样的,就是线程安全的,反之就是线程不安全。

存在线程安全问题

考虑下面一个例子,两个线程对同一个变量自增,使得这个变量的值得到10000.

示例代码

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

    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行结果:

在这里插入图片描述

很遗憾并没有达到我们的预期结果,之所以会这样是因为count++操作不是原子的,具体什么原子性以及如何解决,请看下文。

线程不安全的原因

  1. 多个线程同时修改同一个共享数据,如上述代码修改堆上的count
  2. 操作系统对于线程的调度是抢占式的
  3. 修改操作不是原子的
  4. 内存可见性问题
  5. 指令重排序

原子性

原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。一句话就是要么不做,做的话就是一次性做完。

内存可见性

可见性指,一个线程对共享变量值 的修改,能够及时地被其他线程看到。在Java虚拟机中定义了Java内存模型,其目的就是屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
在这里插入图片描述

  • 线程之间的共享变量存在 主内存 (Main Memory),实际上是内存。

  • 每一个线程都有自己的 “工作内存” (Working Memory) ,这里的内存指的是CPU中 的寄存器或者高速缓存。

  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.。

  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存。

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化。

初始情况:初始情况下, 两个线程的工作内存内容一致

在这里插入图片描述

线程1将空间中的a修改为25,线程1中的值不一定能及时同步到主内存中,对应的线程2的工作内促的值也不一定能及时同步。

在这里插入图片描述

为啥要这么麻烦的拷贝?
因为CPU访问自身的寄存器以及高速缓存的速度,远远超过访问内存的速度(快了3-4个数量级),也就是几千倍,上万倍。

指令重排序

编译器在逻辑等价的前提下,调整代码的执行步骤来提高程序的运行效率。就像某一天你打算先去菜鸟拿U盘,然后回宿舍写作业,然后再和朋友一起去拿快递。这个事情就可以优化成先写作业,然后和朋友一起去菜鸟,顺便把U盘拿了。这样就可以少跑一次菜鸟,这就叫指令重排序。

案例分析

上述代码利用两个线程将一个变量从0自增到10000,但是实际值是小于10000。其原因是因为 线程调度是随机的,造成了线程自增操作的指令集交叉,从而导致实际值小于预期值,至于为啥会造成指令集交叉又因为count++这个操作不是原子的,不是原子意味着不是一气呵成的,而是由三步操作完成:

  1. 从内存把数据读到CPU中的寄存器,该操作记作load
  2. 对数据完成自增,.该操作记作add
  3. 把数据写会内存,该操作记作save

对一个数进行两次自增操作,初始值为0,目标值为2,两个线程并发执行,进行2次子自增。具体线程间指令集可能出现的情况如下:

情况1:线程之间指令集没有任何的交叉,实际值等于预期值。具体如下图所示
在这里插入图片描述

情况2:线程之间指令集存在交叉,实际值小于预期值。具体如下图所示

在这里插入图片描述

在这里插入图片描述

根据上面的分析可知,上述代码出现线程不安全的问题是线程的抢占式执行以及count++操作不是原子性的,由于线程调度是由操作系统所决定,我们无从干涉。那么就只能将不是原子性的操作打包成一个原子性的操作,这样无论线程如何随机的调度,都不会出现bug,至于如何打包,就得通过加锁来解决。

线程加锁

上述的案例告诉了我们一个使用线程的基本问题:你永远不知道一个线程什么时候运行,什么时候不运行。想象一下,你正在吃饭,当你拿筷子夹肉的时候,突然肉就消失不见了,因为你的线程被挂起了,而另一个人在你挂起的期间把那块肉吃了。对于并发工作,我们需要某种方式来防止两个任务同时访问相同的资源。解决这个冲突的方法就是当资源被一个任务使用时,在其上加锁,第一个访问的某项资源的任务必须锁定这个资源,使得其他的任务在被解锁之前,就无法访问它,而解锁之时,另一个任务就会锁定并使用它,以此类推。如果浴室是共享的受限资源,当你冲进去的时候,把门一关获取上锁,其他的人要使用浴室就只能被阻挡,所以就得在浴室门口等待,直到你使用完为止。在Java中提供了synchronized的形式,为防止资源冲突提供了内置支持。当任务要被执行synchronized关键字保护的代码片段时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

synchronized的特性

1. 互斥

synchronized会起到互斥的效果,某个线程执行到某个对象的synchronized中,其他线程如果也执行到同一个对象的synchronized所包含的代码中就会阻塞等待。

public void increase() {
       synchronized (this) {//进入该代码块,相当于针对当前对象"加锁"
           count++;
       }//退出该代码块,相当于针对当前对象"解锁"
    }

synchronized用的锁是存在Java对象头里面,可以简单的理解为,每个对象在内存中存储时,都会有一块内存表示当前"锁定"的状态,相当于记录有没有人使用,如果当前是"无人"状态,那么就可以使用,使用时需要设为"有人"状态,如果当前是"有人"状态,那么其他人无法使用,只能排队。这个排队并不是真正意义上的按顺序来,在操作系统内部会维护一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上,就会阻塞等待,一直等待之前占有锁的线程解锁之后,由操作系统唤醒一个新的线程,再来获取锁,唤醒某个线程并不遵守先来后到的规则,比如A和B线程都在等待C线程释放锁,当C线程释放锁之后,虽然A线程先等待,但是A不一定先获取到锁,而是要和B竞争,谁先抢到就是谁的。

2. 可重入

synchronized所包含的代码块对于线程的来说是可重入的,不会出现自己把自己锁死的情况。所谓自己把自己锁死可以理解为针对同一个对象连续加锁多次,按照之前对锁的设定,第二次加锁的时候,就会阻塞等待,知道第一次的锁是释放,才能获取到第二个锁,但是释放第一个锁也是由该线程完成的,导致该线程就彻底躺平了,啥都干不了,就无法进行解锁的操作。这就是死锁。

    public void increase() {
        synchronized (this) {//加锁
            synchronized (this) {//加锁
                count++;
            }//解锁
        }//解锁
    }

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

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

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

比如上述连续加锁的代码,第一次加锁的时候计数器加一,紧接着第二次又加锁,发现锁的持有者还是自己继续加一,然后就进行两次锁的释放,最终计算器为0时,才是真正的释放锁。

synchronized使用示例

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

体的对象来使用.

1. 直接修饰普通方法 锁的 Counter对象

对上述自增程序尝试使用synchronized加锁,两个线程同时访问的是increase()方法,所以对此方法加锁,实际上对某个对象加锁,该方法属于实例方法此锁的对象就是this。

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

    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行结果:

在这里插入图片描述

2. 修饰静态方法: 锁的 Counter类的对象

示例代码

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    increase();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    increase();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

运行结果:

在这里插入图片描述

3. 修饰代码块:明确指定锁哪个对象
public class Counter {
    private int count = 0;
    public void increase() {
       synchronized (this) {
           count++;
       }
    }

    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter.increase();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行结果:

在这里插入图片描述

再次分析变量自增的案例

当对increase()方法加锁之后,线程1进入该方法时会尝试着去获取锁,一旦获取到锁就会加锁(lock),当退出方法或者退出synchronized所包含的代码块时会释放锁(lock),在线程1持有锁期间,线程2只能干等着,无法进行自增操作,只能等待线程1释放锁,线程2才会进行自增操作。

在这里插入图片描述

注意:两个线程竞争同一把锁才会阻塞等待,如果是获取不同的锁,不会竞争。这就好比,两个男的同时追同一个妹子才会有竞争,否则不存在竞争。

volatile

volatile 修饰的变量, 能够保证 “内存可见性”.

代码在写入volatile修饰的变量的时候

  • 改变线程工作内存中volatile变量的副本的值
  • 将改边后的副本值从工作内存刷新到主内存

代码在读取volatile修饰的变量的时候

  • 从主内存中读取volatile变量的最新值到线程的工作空间
  • 从工作空间中读取volatile变量的副本

加上volatile,会强制读写内存,速度是慢了,但是数据的准确性提高了。

示例代码

import java.util.Scanner;

public class Counter2 {
    public int flags = 0;

    public static void main(String[] args) {
        Counter2 counter = new Counter2();
        Thread t1 = new Thread(() -> {
            while (counter.flags==0) {//该操作对于cpu太快了,所以就直接优化了,第一次读取到了寄存器中后面就没有再从内存中读取

            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
           Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flags =  scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

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

当输入了1线程t1并没有退出,这显然是个bug,给flag加上volatile修饰就可以解决。

 public volatile int flags = 0;

运行结果:

在这里插入图片描述

volatile与synchronized

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

示例代码

public class Counter {
    private  volatile  int count = 0;

    public 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 < 5000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

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

count的值小于预期值,并不能保证原子性。

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

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

相关文章

安卓相关内容

adb环境变量设置完之后&#xff0c;要重启一次“终端”窗口。 adb安装apk到手机 Android开发&#xff1a;用adb命令安装apk到手机-腾讯云开发者社区-腾讯云 This adb servers $ADB_VENDOR_KEYS is not set frida 出现问题&#xff1a;unable to start: address already in …

软件测试面试题——接口自动化测试怎么做?

面试过程中&#xff0c;也问了该问题&#xff0c;以下是自己的回答&#xff1a; 接口自动化测试&#xff0c;之前做过&#xff0c;第一个版本是用jmeter 做的&#xff0c;1 主要是将P0级别的功能接口梳理出来&#xff0c;根据业务流抓包获取相关接口&#xff0c;并在jmeter中跑…

ES开启身份认证

文章目录 X-Pack简介之前的安全方案ES开启认证ES服务升级https协议开启集群节点之间的证书认证 X-Pack简介 X-Pack是Elastic Stack扩展功能&#xff0c;提供安全性&#xff0c;警报&#xff0c;监视&#xff0c;报告&#xff0c;机器学习和许多其他功能。 X-Pack的发展演变&am…

聊聊拉长LLaMA的一些经验

Sequence Length是指LLM能够处理的文本的最大长度&#xff0c;越长&#xff0c;自然越有优势&#xff1a; 更强的记忆性。更多轮的历史对话被拼接到对话中&#xff0c;减少出现遗忘现象 长文本场景下体验更佳。比如文档问答、小说续写等 当今开源LLM中的当红炸子鸡——LLaMA…

开放麒麟1.0发布一个月后,到底怎么样?另一款操作系统引发热议

具有里程碑意义 7月5日&#xff0c;国产首个开源桌面操作系统“开放麒麟1.0”正式发布。 标志着我国拥有了操作系统组件自主选型、操作系统独立构建的能力&#xff0c;填补了我国在这一领域的空白。 举国欢庆&#xff0c;算的上是里程碑意义了&#xff01; 发布后用着如何&a…

Linux系统下U盘打不开: No application is registered as handling this file

简述 系统是之前就安装好使用的Ubuntu14.04&#xff0c;不过由于某些原因只安装到了机械硬盘中&#xff1b;最近新买了一块固态硬盘&#xff0c;所以打算把Ubuntu系统迁移到新的固态硬盘上&#xff1b; 当成功的迁移了系统之后发现其引导有点问题&#xff0c;导致多个系统启动不…

所有流的知识都有,IO流原理及流的分类

1、Java IO流原理 I/O是Input/Output的缩写&#xff0c; I/O技术是非常实用的技术&#xff0c;用于处理设备之间的数据传输。如读/写文件&#xff0c;网络通讯等。 Java程序中&#xff0c;对于数据的输入/输出操作以”流(stream)” 的方式进行。java.io包下提供了各种“流”类…

C++语法(27)--- 类型转换和C++线程库

C语法&#xff08;26&#xff09;--- 特殊类设计_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131879800?spm1001.2014.3001.5501 目录 1.类型转换 1.C语言的转换模式 2.C四种类型转换 1.static_cast 2.reinterpret_cast 3.const_cast …

ALLEGRO之Logic

本文主要讲述ALLEGRO的Logic菜单。 &#xff08;1&#xff09;Net Logic&#xff1a;暂不清楚&#xff1b; &#xff08;2&#xff09;Net Schedule&#xff1a;暂不清楚&#xff1b; &#xff08;3&#xff09;AssignDifferential Pair&#xff1a;暂不清楚&#xff1b; &a…

OR-Tool 报INFEASIBLE

OR-Tool 使用Minimum Cost Flows报 There was an issue with the min cost flow input. Status: Status.INFEASIBLE 这是因为node的编号需要是连续的&#xff0c;比如下面这样不行 修改为连续的

【已解决】如果将MySQL数据库中的表生成PDM

数据库表PDM关系图 | 原创作者/编辑&#xff1a;凯哥Java | 分类&#xff1a;经验分享 有时候&#xff0c;我们需要MySQL数据库中的表生成对应的PDM文件&#xff0c;这里凯哥就讲讲第一种将MySQL数据库的表生成对应的PDM文件。 环境准备&#xff1a; MySQL数据库连接客户端&…

中文多模态医学大模型智能分析X光片,实现影像诊断,完成医生问诊多轮对话

项目设计集合&#xff08;人工智能方向&#xff09;&#xff1a;助力新人快速实战掌握技能、自主完成项目设计升级&#xff0c;提升自身的硬实力&#xff08;不仅限NLP、知识图谱、计算机视觉等领域&#xff09;&#xff1a;汇总有意义的项目设计集合&#xff0c;助力新人快速实…

费舍尔线性分辩分析(Fisher‘s Linear Discriminant Analysis, FLDA)

费舍尔线性分辩分析(Fisher’s Linear Discriminant Analysis, FLDA) 目录 费舍尔线性分辩分析(Fishers Linear Discriminant Analysis, FLDA)1. 问题描述2. 二分类情况3. 多分类情况4. 代码实现4.1 二分类情况4.2 多分类情况 5. 参考资料 1. 问题描述 为解决两个或多个类别的…

ROS-PyQt小案例

前言&#xff1a;目前还在学习ROS无人机框架中&#xff0c;&#xff0c;&#xff0c; 更多更新文章详见我的个人博客主页【前往】 ROS与PyQt5结合的小demo&#xff0c;用于学习如何设计一个界面&#xff0c;并与ROS中的Service和Topic结合&#xff0c;从而控制多个小乌龟的运动…

从零开始搭建Vue3框架(二):Vue-Router4.0使用与配置

前言 上篇文章我们创建了模板项目并成功运行&#xff0c;但是运行后的页面只是一个静态页面&#xff0c;并没有页面间跳转。 对于Vue这种单页应用来说&#xff0c;最要紧的就是控制整个系统的页面路由。因为我们使用Vue3的框架&#xff0c;所以这里使用Vue-Router4.0版本。 …

1992-2021年全国及31省对外开放度测算数据含原始数据和计算过程(无缺失)

1992-2021年全国及31省对外开放度测算数据含原始数据和计算过程&#xff08;无缺失&#xff09; 1、时间&#xff1a;1992-2021年 2、范围&#xff1a;全国及31省 3、指标&#xff1a;进出口总额、国内生产总值、年均汇率 4、计算方法&#xff1a;对外开放度进出口总额/GDP…

【Git系列】Git配置SSH免密登录

&#x1f433;Git配置SSH免密登录 &#x1f9ca;1.设置用户名和邮箱&#x1f9ca;2. 生成密钥&#x1f9ca;3.远程仓库配置密钥&#x1f9ca;2. 免密登录 在以上push操作过程中&#xff0c;我们第一次push时&#xff0c;是需要进行录入用户名和密码的&#xff0c;比较麻烦。而且…

【数据分析专栏之Python篇】四、pandas介绍

前言 在上一篇中我们安装和使用了Numpy。本期我们来学习使用 核心数据分析支持库 Pandas。 一、pandas概述 1.1 pandas 简介 Pandas 是 Python 的 核心数据分析支持库&#xff0c;提供了快速、灵活、明确的数据结构&#xff0c;旨在简单、直观地处理关系型、标记型数据。 …

Resnet与Pytorch花图像分类

1、介绍 1.1数据集介绍 flower_data├── train│ └── 1-102&#xff08;102个文件夹&#xff09;│ └── XXX.jpg&#xff08;每个文件夹含若干张图像&#xff09;├── valid│ └── 1-102&#xff08;102个文件夹&#xff09;└── ─── └── XXX.jp…

如何使用免费敏捷工具Leangoo领歌管理Sprint Backlog

什么是Sprint Backlog&#xff1f; Sprint Backlog是Scrum的主要工件之一。在Scrum中&#xff0c;团队按照迭代的方式工作&#xff0c;每个迭代称为一个Sprint。在Sprint开始之前&#xff0c;PO会准备好产品Backlog&#xff0c;准备好的产品Backlog应该是经过梳理、估算和优先…