线程安全

文章目录

  • 观察线程安全问题
  • 线程安全的概念
  • 出现线程安全问题的原因
    • 共享数据
    • 原子性
    • 总结
  • 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.hqwc.cn/news/206654.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2011-2022年地级市互联网普及率数据

2011-2022年地级市互联网普及率数据 1、时间&#xff1a;2011-2022年 2、指标&#xff1a;行政区划代码、年份、地区、互联网宽带接入用户_千户、常住人口数_千人、户籍人口数_千人、每百人互联网宽带用户_常住人口口径、每百人互联网宽带用户_户籍人口口径 3、来源&#xf…

触发设备离线

业务场景 业务开发过程中&#xff0c;我们经常会需要判断远程终端是否在线&#xff0c;当终端离线的时候我们需要发送消息告知相应的系统&#xff0c; 环形队列 1.创建一个index从0到30的环形队列&#xff08;本质是个数组&#xff09; 2.环上每一个slot是一个Set&#xf…

【MySQL】多表查询、子查询、自连接、合并查询详解,包含大量示例,包你会。

复合查询 前言正式开始一些开胃菜多表查询自连接子查询单行子查询多行子查询in关键字all关键字any关键字多列子查询在from中使用子查询 合并查询union 和 union all 前言 我前面博客讲的所有的查询都是在单表中进行的&#xff0c;从这里开始就要专门针对查询这个话题进行进一步…

HTML5+CSS3+JS小实例:霁青+翠蓝的Tabbar动画特效

实例:霁青+翠蓝的Tabbar动画特效 技术栈:HTML+CSS+JS 字体图标库:Font Awesome 效果: 源码: 【HTML】 <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta nam…

虚拟摇杆OnJoystickMove未被调用,角色不移动

更改interaction type 为 event notification

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

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

12、人工智能、机器学习、深度学习的关系

很多年前听一个机器学习的公开课,在Q&A环节,一个同学问了老师一个问题“机器学习和深度学习是什么关系”? 老师先没回答,而是反问了在场的同学,结果问了2-3个,没有人可以回答的很到位,我当时也是初学一脸懵,会场准备的小礼品也没有拿到。 后来老师解释“机器学习和…

基于单片机电梯液晶显示防超重,防气体报警、防夹报警控制系统及源程序

一、系统方案 1、本设计采用51单片机作为主控器。 2、液晶显示楼层。 3、防超重&#xff0c;防气体报警、防夹报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 /lcd1602初始化设置*/ void init_1602() //lcd1602初始化设置 { write_co…

关于ElectronVue3中集成讯飞星火AI

前言&#xff1a;我的最终目的是为了在QQ上集成一个AI机器人&#xff0c;因此在这里先实现一个简单的集成 先上效果图 总体还是很简单的&#xff0c;我在调用websock获取回复内容的基础上另外集成了一个事件总线&#xff0c;让我们在调用获取消息的时候能够更加方便快捷 工具代…

表格制作软件排行榜,热门做表格的软件推荐

在数字化时代&#xff0c;表格不仅仅是企业管理和数据整理的重要工具&#xff0c;更是学术研究、项目规划以及日常生活中必不可少的一部分。为了更高效地进行表格制作&#xff0c;选择一款优秀的表格制作软件是至关重要的。在众多的软件中&#xff0c;我们特别推荐一款备受好评…

“KeyarchOS:国产Linux新星的崛起与创新之路“

简介 KeyarchOS是一款由浪潮信息自主研发的服务器操作系统。它因为几个特点而受到我的青睐和一些用户的关注。 首先&#xff0c;KeyarchOS注重安全性和稳定性。它有一些防护和隔离功能&#xff0c;来帮助系统稳定运行&#xff0c;而且是中文语言更接地气。 其次&#xff0c;Ke…

银行数字化转型导师坚鹏:BLM银行数字化转型战略培训圆满结束

在数字化转型背景下&#xff0c;中国金融出版社金融文化研训院为了落实监管政策《关于银行业保险业数字化转型的指导意见》&#xff0c;充分认识到学习银行银行数字化转型战略的价值和重要性&#xff0c;特别举办《2023年金融机构数字化转型及数字化风控与运营管理研讨班》。为…