Java线程安全中的原子性浅析
作者:绿仔牛奶_ 发布时间:2021-09-06 16:08:13
何为原子性
原子性:一条线程在执行一系列程序指令操作时,该线程不可中断。一旦出现中断,那么就可能会导致程序执行前后的结果不一致。与数据库中的原子性(事务管理体现)是相同的
概括:一段程序只能由一条线程去完整的执行,不能被多个线程干扰执行
以最经典的转账为例,甲向乙的账户转账500这个转账行为就包含了两个操作:分别是1. 甲的账户-500 2. 乙的账户 +500但如果此时不能保证原子性操作就可能会出现甲的账户减了500但是乙的账户没有加500的情况
首先我们先来区分哪些是原子操作哪些非原子操作:
int a = 1; // (1)
int b = a; // (2)
a += b; // (3)
(1)一个操作,就是将值“1” 赋给 变量a
(2)两个操作,首先获取a的值,然后将a的值赋给b
(3)四个操作,首先获取b的值,再获取a的值,再将a与b的值相加,再将相加后的值赋给a
所以只有(1)和(3)属于原子操作,(2)不构成原子操作
上述举例,我们清楚了原子操作就是一个不可分割的操作。
下面我们来看线程安全的原子性示例:
测试代码:
public class Demo{
public static void main(String[] args) {
Temp task = new Temp();
// 启动100条线程
for (int i = 1; i <= 100 ; i++) {
new Thread(task).start();
}
}
}
class Temp implements Runnable{
private int count = 0;
@Override
public void run() {
// 线程任务:将count
for (int i = 1; i <= 100; i++) {
count++;
System.out.println(Thread.currentThread().getName()+"::count====>>"+count);
}
}
}
上述代码实现将Count从0加到10000,每一条线程控制count加100,100条线程启动执行实现。但是在执行过程中会发现会出现几次无法达到10000的结果,产生的原因就是假设当某一条线程在执行count累加时执行到了count=97时(也就是该线程执行失败,并且提交了执行失败的结果)cpu被其他线程拿到,那么其他线程继续拿到count=97进行累加,这样就导致最后结果不准确这个时候线程就不是安全的,也就是说我们这段程序是不具备原子性的。
但是有时效果不是很明显,建议可以将线程数增加到500条;
那么原子性的问题如何解决呢?
解决方法
加锁–> 悲观锁(阻塞同步)
利用synchronized修饰的同步方法、同步代码块啥的,随便你怎么上锁都可以,本质上的解决机理就是保证线程任务同时只能被一条线程执行,在执行完毕之前其他线程无法拿到执行权。由此来保证线程的原子性
从时间维度上来讲,某一时刻只允许一条线程执行线程任务,其他线程就处于阻塞状态,这种利用阻塞其他线程的方式就称为阻塞同步也叫做互斥同步。大大降低了执行效率和性能
这种解决方式采用了悲观的并发策略,synchronized也被称为悲观锁,为什么说是悲观?因为程序在加锁之初就默认每一次线程操作共享资源时都会被其他线程干扰。即在不进行同步干预的情况下,程序默认每次线程操作共享资源都会存在其他线程的竞争继而导致程序执行出现问题。阻塞同步也就是做出了最坏的打算,故称为悲观锁
使用原子类( Atomic )–> 乐观锁(非阻塞同步)
为什么还要使用原子类?
because 利用synchronized加锁去解决原子性问题,性能太低了。如果我们的程序线程数量太大,那每次线程任务只能被单条线程执行,效率太低了
原子类是性能高效、线程安全。并且Java提供了比较全面的原子类供开发者使用,下面以AtomicInteger为例来说它的方法使用,多数原子类的方法都有些类似
// 导包
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger整型原子类
// 常用方法
public final int set(int newValue) //为当前对象赋值
public final int get() //获取当前值
public final int getAndSet(int newValue)//获取当前值然后重新赋值
public final int getAndIncrement()//先获取当前值然后自增
public final int getAndDecrement() //先获取当前值然后自减
public final int getAndAdd(int delta) //获取当前值然后加上delta
boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update)
除了整型原子类之外还有如长整型原子类AtomicLong、布尔原子类AtomicBoolean、整型数组AtomicIntegerArray、长整型数组AtomicLongArray、引用数据类型数组AtomicReferenceArray等
CAS机制(Compare And Swap)
什么是CAS?
CAS( Compare And Swap )意为比较并交换,CAS的实现机制就是利用了非阻塞同步。
采用乐观的并发策略,当程序运行中,我们不再利用阻塞其他线程来保证当前线程正确执行的方式,而是作出最优情况的解决方案,故而也被称为乐观锁。
即每一次线程操作共享资源都会执行成功并提交正确的结果,但是在将最新的结果提交时,需要与当前存储的值进行比较就是进行一个新值与原值冲突检测,比较完之后如果发现最新结果与当前值一致则说明执行失败,反之则是执行成功然后替换到原值即可
我们依然用上述操作count作为示例,如下图所示
可以看出,不论任何一条线程正在操作count,都可以与其他线程进行竞争。非阻塞同步大大的节省了线程阻塞和唤醒的性能开销
下面谈一下CAS具体的实现机制(CAS算法)
CAS实现主要用到三个操作数,分别是 内存地址S、原值(预期值)A、新值B
当线程向主内存中提交一个共享变量的新值B时,首先会将原值A与S处存储的值进行比较,只有当两个值相同时(没有其他线程干扰),才能将新值B提交至主内存更新共享变量
CAS算法真正关注的是线程提交时的S处值与预期值是否相同,但仍然存在ABA漏洞,暂时不做深究
来源:https://blog.csdn.net/yuqu1028/article/details/128608067


猜你喜欢
- 看到群里还有小伙伴说公司里还特别建了800+人的群在处理...好在很快就有了缓解措施和解决方案。同时,log4j2官方也是速度影响发布了最新
- 本文首先将会回顾Spring 5之前的SpringMVC异常处理机制,然后主要讲解Spring Boot 2 Webflux的全局异常处理机
- 我们都知道,在我们开发时需要在模拟器上模拟GPS,可在Location的时候总是null,上网查了一下,发现如下解决: 网上大侠的解决方案:
- 使用对象访问类中的成员:对象名.成员变量;对象名.成员方法();成员变量的默认值:具体实例代码:public class StudentTe
- 最近接触到一个需求要求压缩导出文件,于是乎便要致力于研究一下工具类啦,故也诞生了此篇文章。下面代码中,溪源也将import导入的依赖也贴出来
- 本文实例为大家分享了C#基于Socket的TCP通信实现聊天室的具体代码,供大家参考,具体内容如下一、Socket(套接字)通信概念套接字(
- 在 Lock 接口中,获取锁的方法有 4 个:lock()、tryLock()、tryLock(long,TimeUnit)、lockInt
- 一、项目搭建1、可以在新建项目的使用Spring MVC框架。或者创建一个简单的项目之后再用Add Framework Support来添加
- 参考资料《Java 编程思想》,关于含有基类的导出类,其成员的初始化过程是一个容易让人困惑的地方,下面通过具体的实例进行讲解,代码取自《Ja
- 一、前言 验证码可以说在我们生活中已经非常普遍了,任何一个网站,任何一个App都
- Java8新特性系列我们已经介绍了Stream、Lambda表达式、DateTime日期时间处理,最后以“NullPointerExcept
- 最近要实现一个功能,就是checkbox跨页多选,在网上看了一下,资料很少,而且大多是不完全的。不过经过我的努力,终于做出来了。
- 1 需求Mybatis-plus使用@TableLogic注解进行逻辑删除数据后,在某些场景下,又需要查询该数据时,又不想写SQ
- 1.问题由来迷宫实验是取自心理学的一个古典实验。在该实验中,把一只老鼠从一个无顶大盒子的门放入,在盒中设置了许多墙,对行进方向形成了多处阻挡
- Java事件处理机制和适配器最重要的是理解事件源,监视器,处理事件的接口的概念。1.事件源:是能够产生时间的对象都可以叫事件源,比如文本框,
- 引言大家应该都知道,对Excel表格设置分页对我们预览、打印文档时是很方便的,特别是一些包含很多复杂数据的、不规则的表格,为保证打印时每一页
- 字节数组的关键在于它为存储在该部分内存中的每个8位值提供索引(快速),精确的原始访问,并且您可以对这些字节进行操作以控制每个位。 坏处是计算
- zip 是一个非常常见的压缩包格式,本文主要用于说明如何使用代码 文件或文件夹压缩为 zip压缩包及其解压操作,我们采用的是 微软官方的实现
- 前言由于业务需要,后端需要返回一个树型结构给前端,包含父子节点的数据已经在数据库中存储好,现在需要做的是如何以树型结构的形式返给给前端。数据
- 1、如何解决服务之间的通信问题?[1]HTTP REST方式 使用http协议进行数据传递 json格式数据[2]RPC方式 远程过程调用