【Java EE 初阶】如何保证线程安全

news/2025/2/13 0:11:14/

目录

 

1.线程是什么?

2.线程安全(重点)

1.概念:

1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期

那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:

1.多个线程修改了同一个变量

2.线程在CPU调度上是抢占式执行的

3.没有保证线程的原子性

 4.内存可见性

JAVA内存模型JVM是什么?

为什么要用JVM呢?

 5.代码的有序性

我们来总结一下造成线程不安全的原因主要有哪些? 

3.解决线程不安全问题(Synchronized关键字-监视器锁)

核心代码展示:对count自增的方法上锁

结果展示:

4.Synchronized关键字的注意事项

在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域

5.Synchronized的使用


 

1.线程是什么?

Thread是Java中的类,PCB是系统中真正的线程

调用start()方法才会真正的去系统中申请一个线程

线程的状态有哪些? 

d41fca42e3e7489ba7a0b99610e551f8.png

2.线程安全(重点)

1.概念:

如果多线程环境下代码运行的结果与单线程环境下相同,则说明这个线程是安全的

1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期

5ceb8680f07a47728236b788ea575f40.png

可以看见结果并不是我们所想的100000

那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:

1.多个线程修改了同一个变量

2.线程在CPU调度上是抢占式执行的

3.没有保证线程的原子性

原子性就是一段代码,要么全部执行成功,要么全部执行失败

我们知道,代码最终都会编译成CPU可以执行的指令,那么count++这一条指令其实对应着三步操作:

1.从内存把数据读到CPU

2.进行数据更新

3.把数据写回CPU

那么为什么不保证原子性就会出错呢?首先这和线程的抢占式执行密切相关

我们来举个例子:

分别有两个线程对count这个变量进行自增操作t1和t2,count初始值为0

假设t2先从内存把数据读到CPU,此时CPU被t1抢占了过去,t1开始从内存把数据读到CPU并对count进行了自增操作:count++,此时t2又把CPU抢占了回来,也对count进行了自增操作,count++,此时又回到t1,将数据写回CPU,count = 1,然后t2也进行了数据的写回,count= 1;现在我们可以发现问题了,由于没有保证原子性,导致多个线程之间进行了重复的自增操作,覆盖掉别的线程修改后的值,所以最终的结果不对

图解如下:

9108dca3deb5480389d37aefb027435a.png

 4.内存可见性

在多线程环境下,某一个线程修改了共享的变量,其他线程不能及时地接收到最新的值

JAVA内存模型JVM是什么?

99cfac92f5474ccbbbc608e91c326167.png

1.主内存:指的是硬件的内存条,进程在启动的时候会申请一些资源,包括内存资源,用来保存所有的变量

2.工作内存:指的是线程独有的内存空间,它们之间不能相互访问,起到了线程之间内存隔离的作用

3.JVM规定,一个线程在修改某个变量的时候,必须把这个变量从主内存加载到自己的工作内存,修改完成后在返回主内存

为什么要用JVM呢?

Java是一个跨平台语言,而JVM把不同的计算设备和操作系统对内存的管理做了一个统一的封装

 5.代码的有序性

是指在编译过程中,JVM调用本地接口,CPU执行指令的过程中,指令的有序性

3eb28da366b0425fb8009f12740941a2.png

 这种通过打乱顺序,把不相关的代码或指令重新排列,从而提高程序效率的方式,在程序中叫做指令重排序

在单线程中重排序之后的结果百分百正确,但在多线程环境下就未必了

我们来总结一下造成线程不安全的原因主要有哪些? 

f08d9ab219284248948289b514036d13.png

3.解决线程不安全问题(Synchronized关键字-监视器锁)

对当前执行的代码加锁

当某一个线程要执行这个方法时,就先获取锁,获取到锁之后再去执行代码,其他线程也需要执行这个方法时,也需要获取锁,但是已经有线程持有锁时,他就需要等待,等到上一个线程释放这把锁(注意分辨锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系)之后才有可能竞争到CPU的调度

因为线程时抢占式执行,CPU对调度是随机的,并不是先阻塞等待的线程就一定会先拿到锁

我们发现上了锁之后的代码变成了单线程,这也是不可避免的

所以在获取数据时,我们用多线程来提高效率

在修改数据时,用Synchronized上锁来保证安全

核心代码展示:对count自增的方法上锁

public synchronized void increase() {count++;}

结果展示:

c6e944af48db4912a2434172b544cb77.png

可以看见上锁之后结果正确了,说明Synchronized解决了原子性的问题,并且通过把并行操作变成了串行操作,也保证了内存的可见性

但是Synchronized并不能保证有序性

4.Synchronized关键字的注意事项

1.从并行到串行:首先要保证正确,然后才考虑效率

2.加锁与CPU调度:锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系

3.加锁的范围:比如加在for循环外就与串行是一模一样的,但加在外面两个for循环之间就是并发执行的,这样写要比两个for循环分别执行要快很多,这个要跟及实际情况考虑,加锁的范围越大称锁的粒度越大,反之越小

4.给代码块加锁:Synchronized可以用来修饰代码块,此时需要传入一个参数,用来表明锁的对象

5.锁对象:竞争的锁都必须是针对同一个对象的,可以自定义一个单独的对象表示锁(可以是JAVA中的任何对象),也可以使用this,多线程并不关心锁的对象是哪个,只关心他们是否竞争同一把锁,也就是是否是同一个对象,只有同一个对象才会产生锁的竞争

在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域

  • mark word 与 类型指针(_class)  这两部分统一称为对象头 主要描述了当前是哪个线程获取到的资源,记录的是线程对象信息,当线程释放所资源的时候就会把线程对象信息清除掉,其他的线程就可以继续获得锁资源
  • 实例数据 保存的就是类中的属性
  • 对其填充 每一个类对象占用的字节数必须是八字节的整数倍

5.Synchronized的使用

6b8be3966b7e498686174e6e9c20af7b.png

 989469be810641a8a640f19b329e0705.png

 

 


http://www.ppmy.cn/news/62074.html

相关文章

消息队列中的事务消息

大家好,我是易安!今天我们谈一谈消息队列中的事务消息这个话题。 一说起事务,你可能自然会联想到数据库。我们日常使用事务的场景,绝大部分都是在操作数据库的时候。像MySQL、Oracle这些主流的关系型数据库,也都提供了…

K8s基础8——svc基础使用、应用暴露、iptables代理、ipvs代理

文章目录 一、Service基本了解二、Service定义与创建2.1 相关命令2.2 yaml文件参数大全2.3 创建svc2.3.1 两种创建方式类比2.3.2 验证集群内A应用访问B应用2.3.3 将集群外服务定义为K8s的svc2.3.4 分配多个端口 2.4 常用三种类型2.4.1 ClusterIP(集群内部访问&#…

持续测试:DevOps时代质量保证的关键

在 DevOps 时代,持续测试已成为质量保证的一个重要方面。近年来,软件开发方法论发生了快速转变。随着 DevOps 的出现,已经发生了向自动化和持续集成与交付 (CI/CD) 的重大转变。传统的质量保证方法已不足以满足现代软件开发实践的需求。持续测…

基于微信小程序的酒店预定管理系统设计与实现

第1章 绪论 1 1.1开发背景与意义 1 1.2开发方法 1 1.3论文结构 1 2系统开发技术与环境 3 2.1 系统开发语言 3 2.2 系统开发工具 3 2.3 系统页面技术 3 2.4 系统数据库的选择 4 2.5 系统的运行环境 4 2.5.1 硬件环境 4 2.5.2 软件环境 4 3系统分析 5 3.1可行性分析 5 3.1.1 经济…

JUC并发编程与源码分析笔记13-AbstractQueuedSynchronizer之AQS

前置知识 公平锁和非公平锁可重入锁自旋思想LockSupport数据结构之双向链表设计模式之模板设计模式 AQS入门级别理论知识 是什么 AbstractQueuedSynchronizer:抽象的队列同步器。 用来实现锁或其他同步器组件的公共基础部分的抽象实现,是重量级基础框…

IDP中的黄金路径究竟是什么?

在云原生时代,开发人员面临着越来越多的工具、技术、思维方式的选择,给他们带来了极大的认知负担和工作量。为了提高开发人员的开发效率与开发体验,一些头部科技公司开始建立自己的内部开发者平台(IDP)。在之前的文章我…

阿里云CentOS服务器安装Redis教程(一步步操作)

使用阿里云服务器ECS安装Redis数据库流程,操作系统为CentOS 7.6镜像,在CentOS上安装Redis 4.0.14,云服务器选择的是持久内存型re6p实例,新手站长分享阿里云CentOS服务器安装Redis流程方法: 目录 在CentOS系统中部署R…

C++容器适配器stack和queue(含deque,priority_queue)

目录 1.容器适配器 1.1 什么是适配器 1.2 STL标准库中stack和queue底层结构 1.3 deque 1.3.1 deque原理介绍(了解) 1.3.2 deque优点和缺点 1.3.3 为什么选择deque作为stack和queue的底层默认容器 2. stack介绍和使用 2.1 stack介绍 2.2 stack使用 2.3 …