原来这就是线程安全(一)

@TOC

一:什么是线程不安全??

先看一段代码:

public class Demo1 {public static int count = 0;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);}
}

在这里插入图片描述
上面的代码可能我们以为最终的count=100000;
但最终小于100000,这就是线程安全在搞鬼.
因为线程是随机调度 的,抢占式执行,就会使代码执行结果不可控,结果也就是随机的.
我们往往希望结果使我们所预期的,而这种不可控,随机的执行结果就称为bug.
多线程代码,引起了bug,这样的问题就是"线程安全问题",存在线程安全问题的代码,就称为"线程不安全".

二:为什么代码执行结果不是100000

count++操作其实是三个指令:
(1)把内存中的count中的数值,读到CPU寄存器中.(load)
(2)把寄存器中的值+1,(add).
(3)把寄存器上述计算后的值,写回到内存的count里.(save)
但因为不止一个线程,某个线程在执行指令的过程中,当它执行到任何一个指令的时候,由于随机调度,抢占式执行,就有可能被其他线程把它的CPU给抢占走.(操作系统把前一个线程调度走,后一个线程执行).
下面列举两个线程并发执行的时候,可能的执行指令的顺序,(不同的执行顺序,得到的结果可能存在差异)
在这里插入图片描述

在这里插入图片描述

我们知道count数据是存在内存中的,而同一个进程,不同的线程是共用一块内存资源的,也就是t1,t2线程操作的是同一块内存地址.
以下图为例:
在这里插入图片描述
1: t1线程 load操作拿到count的值,拿到了0,放到了A寄存器中;
2:t2线程 load 操作拿到count的值,拿到了0,放到了B寄存器中;
3:t1线程执行add操作,将A寄存器中的值+1,
4:t2线程执行add操作,将B寄存器中的值+1,
5:t1线程执行save 操作,将A寄存器中的值写回到内存中,内存中的值由0变为1.
6:t2线程执行save 操作,将B寄存器中的值写回到内存中,内存中的值由1变为1.

三:出现线程不安全的原因:

1:线程在系统中是随机调度,抢占式执行的,
2:当前代码中,多个线程同时修改同一个变量.

如果只有一个线程,那么修改操作没问题.
如果多个线程是读取操作,那么也没事.
如果多个线程修改不同的变量,也没事.
3:线程针对变量的修改操作不是"原子"的,而是三条指令.
但有的修改操作,是原子的,比如直接对变量进行赋值操作(CPU中就一个move指令).
4:内存可见性问题,引起的线程不安全
5:指令重排序,引起的线程不安全

四:解决线程不安全问题

线程不安全原因1:我们不能修改,因为硬件方面就是这样设计的(内核就是这样设计的 ).
原因2:不能修改,因为此时场景就是多线修改同一个变量.
原因3:可以通过"加锁"操作,将多个指令打包成一个整体.
通过"加锁",就达到了"互斥"的效果
互斥:t1线程在执行的时候,t2线程不能执行(阻塞等待),t2线程在执行的时候,t1线程不能执行(阻塞等待),互斥也称为锁竞争,锁冲突.

1:锁的操作:

(1)加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待(都是系统内核控制)(在Java中可以看到BLOCKED状态)
(2)解锁:直到t1解锁了之后,t2才有可能拿到锁(加锁成功).

2:编写代码

首先创建一个对象,使用这个对象作为锁:
在Java中可以使用任何对象作为加锁对象.
创建锁对象的意义:
锁对象的用途:有且只有一个,那就是用来区分,多个线程是否针对同一个对象(count)加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待.
通过设定不同的锁对象,来确定竞争关系.

public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Thread t1=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " +count);}
}

2.1 synchronized (){}

()里面写"锁对象"
{}:当进入代码块,就相当于对锁对象进行了加锁操作.
当出了代码块,就相当于对锁对象进行了解锁操作.

public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker){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 = " +count);}
}

上述代码就相当于两个线程,针对同一个锁对象加锁,就会产生互斥.
t1,t2做的事情就是:判断循环条件,加锁,load ,add, save ,解锁,i++;
假设:由于是线程是并发执行的,这里假设t1线程先执行到了加锁操作,并且t1还没解锁,那么t2线程就不能获得锁对象,就不能进行加锁操作,
假设t1刚解锁,t2 就加锁了,但t1线程并不是什么代码也不执行,而是继续执行i++,循环判断条件,当t1执行到了synchronized,发现不能获得锁对象,那么t1线程只好阻塞等待了.
因此:在t1,t2两个线程中,每次count++是存在锁竞争的,会变成"串行"执行,但是执行for 循环中的条件以及i++仍然是并发执行的.

public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Object locker2=new Object();Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " +count);}
}

上述代码就是两个线程针对不同对象加锁,就不会产生互斥,也就不会发生阻塞等待现象了.
操作系统中的加锁,解锁功能,核心还是CPU提供的指令(硬件提供了这样的能力,软件上才有对应的功能)

3:加锁的变种写法:

3.1:创建一个自定义类,并把这个类的引用作为加锁对象

class Count {public int count;public void add(){count++;}public int get(){return count;}
}
public class Demo1 {public static void main(String[] args) throws InterruptedException {Count count=new Count();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {synchronized (count){count.add();}}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {synchronized (count){count.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count= " +count.get());}
}

3.2: synchronized (this){}

class Count1 {public int count;public void add(){synchronized (this){count++;}}public int get(){return count;}}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}

3.3: synchronized public void add(){}

class Count1 {public int count;synchronized public void add(){count++;}public int get(){return count;}}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}

3.4:针对类对象加锁

class Count1 {public static int count;static void func(){synchronized (Count1.class){count++;}}public int get(){return count;}public class Demo2 {}public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Count1 count2=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.func();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count2.func();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}

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

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

相关文章

【C语言】——指针六:冒泡排序与qsort函数的实现

【C语言】——指针六&#xff1a;冒泡排序与qsort函数 一、冒泡排序1.1、冒泡排序的原理1.2、用代码实现冒泡排序 二、qsort函数2.1、qsort函数的定义2.2、 qosrt函数的使用&#xff08;1&#xff09;比较函数的写法&#xff08;2&#xff09;使用 q s o r t qsort qsort 函数…

C语言编译与链接

前言 我们想一个问题&#xff0c;我们写的C语言代码都是文本信息&#xff0c;电脑能直接执行c语言代码吗&#xff1f;肯定不能啊&#xff0c;计算机能执行的是二进制指令&#xff0c;所以将C语言转化为二进制指令需要一段过程&#xff0c;这篇博客讲一下编译与链接&#xff0c;…

基于AI网关的光伏电站在线监测方案

光伏电站作为利用太阳能的重要方式&#xff0c;凭借其环保、高效和可持续性的优势&#xff0c;在全球范围内得到广泛应用。然而&#xff0c;光伏电站的运营和维护也面临着诸多难点和痛点。在这一背景下&#xff0c;AI智能网关的应用为光伏电站的运营和维护带来了新的突破。 光伏…

数据结构进阶篇 之 【二叉树链序存储】的整体实现讲解

封建迷信我嗤之以鼻&#xff0c;财神殿前我长跪不起 一、二叉树链式结构的实现 1.二叉树的创建 1.1 手动创建 1.2 前序递归创建 2.二叉树的遍历 2.1 前序&#xff0c;中序以及后序遍历概念 2.2 层序遍历概念 2.3 前序打印实现 2.4 中序打印实现 2.4 后序打印实现 2.…

在低成本loT mcu上实现深度神经网络端到端自动部署-深度神经网络、物联网、边缘计算、DNN加速——文末完整资料

目录 前言 DNN 量化神经网络 并行超低功耗计算范式 面向内存的部署 结果 原文与源码下载链接 REFERENCES 前言 在物联网极端边缘的终端节点上部署深度神经网络( Deep Neural Networks&#xff0c;DNNs )是支持普适深度学习增强应用的关键手段。基于低成本MCU的终端节点…

Paper Digest|基于在线聚类的自监督自蒸馏序列推荐模型

论文标题&#xff1a; Leave No One Behind: Online Self-Supervised Self-Distillation for Sequential Recommendation 作者姓名&#xff1a; 韦绍玮、吴郑伟、李欣、吴沁桐、张志强、周俊、顾立宏、顾进杰 组织单位&#xff1a; 蚂蚁集团 录用会议&#xff1a; WWW 2024 …

【Linux】开始学习进程替换吧!

送给大家一句话&#xff1a; 人生中有些事&#xff0c;你不竭尽所能去做&#xff0c;你永远不知道你自己有多出色。—— 尾田荣一郎《海贼王》 开始学习进程替换吧 1 前言2 进程替换2.1 替换函数2.2 替换原理2.3 单进程改为多进程2.4 理解使用exec* 函数int execl (const char …

【独立开发前线】Vol.25 Dogacade-通过SEO,每个月13万的访问量

今天要给大家分享的一个案例网站是&#xff1a;Dogacade 网址是&#xff1a;Dog Academy - Home 这是一个提供狗狗训练服务的网站&#xff0c;网站的宣传语是&#xff1a;在短短 4 周内培育出您梦想中的听话、乖巧的狗狗。 网站的流量非常不错&#xff0c;在这么垂直利基的市…

链表中两两交换结点(力扣24)

文章目录 题目题解一、思路二、解题方法三、Code 总结 题目 Problem: 24. 两两交换链表中的节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交…

24/03/28总结

抽象类&#xff1a; 将共性的方法抽取到父类之后。由于每一个子类执行的内容是不一样&#xff0c;所以&#xff0c;在父类中不能确定具体的方法体。该方法就可以定义为抽象方法。 而为什么不直接在子类中定义方法&#xff1a;项目的完成不是一个人&#xff0c;如果有时忘记写方…

BaseDao封装增删改查

文章目录 什么是BaseDao操作代码增删改查询单个数据查询多个数据 总结 什么是BaseDao BaseDao是&#xff1a; 数据库里负责增加&#xff0c;删除&#xff0c;修改&#xff0c;查询 具体来说是一种接口代码,公共方法的接口类。 在dao层新建basedao,其他dao层接口继承basedao 相…

python函数参数中独立星号*的作用

python函数中间有一个&#xff08;&#xff09;分隔&#xff0c;星号后面为*命名关键字参数&#xff0c;星号本身不是参数**。命名关键字参数&#xff0c;在函数调用时必须带参数名字进行调用。如下例子&#xff1a;