CASAtomic原子操作详解

一、CAS(Compare And Swap)

1、CAS介绍

CAS原理:假设有三个值,E(旧值)、U(需要更新的值)、V(内存中真实的值),具体参照下图:

作用:解决线程轻微竞争场景,同一时间只有一个线程能进入CAS代码块中,其它线程空转循环

compareAndSwapInt()方法对不同系统CAS指令的包装,Intel的汇编指令cmpxchg,不同厂家所实现的具体算法不一样

2、举例

public class UnsafeFactory {
	
	/**
	 * 通过反射获取Unsafe属性
	 * @return
	 */
	public static Unsafe getUnsafe() {
		Field theUnsafe;
		try {
			theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
			theUnsafe.setAccessible(true);
			// 因为theUnsafe是静态属性 所以field.get(Object)参数传什么都可以
			return (Unsafe) theUnsafe.get(null);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	/**
	 * 找到指定类属性在内存中偏移的地址
	 * @param clazz
	 * @param fieldName
	 * @return
	 */
	public static long getFieldOffset(Class clazz, String fieldName) {
		try {
			return getUnsafe().objectFieldOffset(clazz.getDeclaredField(fieldName));
		} catch (Exception e) {
			throw new Error(e);
		}
	}
}

public class CASTest {

	public static void main(String[] args) {
		Entity entity = new Entity();
		
		Unsafe unsafe = UnsafeFactory.getUnsafe();
		
		long fieldOffset = UnsafeFactory.getFieldOffset(Entity.class, "x");
		System.out.println(fieldOffset);
		
		System.out.println(unsafe.compareAndSwapInt(entity, fieldOffset, 0, 1));
		
		System.out.println(unsafe.compareAndSwapInt(entity, fieldOffset, 1, 2));
		
		// 这个时候内存中的值已经改成2,所以0改成3是不能改成功的
		System.out.println(unsafe.compareAndSwapInt(entity, fieldOffset, 0, 3));
	}
}

class Entity {
	// markword占8字节 klasspointer默认开启指针压缩占4字节 x属性的偏移量就是12
	int x;
}

打印结果:

12
true
true
false 

3、存在问题

1)激烈竞争线程空转导致性能下降

如果存在大量线程竞争一个变量,必然导致其它线程资源,或者长期CAS失败的线程,都会给CPU调度产生性能问题

2)ABA问题

可以加一个版本号区分究竟做了多少次版本的修改

3)只能对一个值做原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但对多个共享变量操作时,循环CAS无法保证操作的原子性,这个时候可以用锁

二、Atomic原子操作类

1、使用

在java.util.concurrent.atomic包里提供了一组原子操作类:

  • 基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
  • 引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、 AtomicReferenceFieldUpdater
  • 原子类型累加器(jdk1.8增加的类):DoubleAccumulator、DoubleAdder、 LongAccumulator、LongAdder、Striped64
1)原子基本类型
public class AtomicIntegerTest {

	static AtomicInteger atomicInteger = new AtomicInteger();
	
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			Thread thread = new Thread() {
				@Override
				public void run() {
					for (int j = 0; j < 10000; j++) {
						atomicInteger.incrementAndGet();
					}
				}
			};
			thread.start();
			thread.join();
		}
		System.out.println(atomicInteger.get());
	}
}

打印结果:100000 

incrementAndGet()方法就是通过CAS循环读取AtomicInteger类的value属性在内存中的值,直到加1成功,跳出while循环,返回旧值

2)原子更新数组类型
public class AtomicIntegerArrayTest {

	static int[] array = {10, 21, 9, 32, 99};
	static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
	
	public static void main(String[] args) {
		// 设置下标为0的元素为100
//		atomicIntegerArray.set(0, 100);
		int andSet = atomicIntegerArray.getAndSet(0, 100);
		// 这里返回原值 实际值是100
		System.out.println(andSet);
		int i = atomicIntegerArray.get(0);
		System.out.println(i);
		int andAdd = atomicIntegerArray.getAndAdd(1, 9);
		// 这里也是一样 返回的是原值 实际是加9之后的值
		System.out.println(andAdd);
		int j = atomicIntegerArray.get(1);
		System.out.println(j);
	}
}

打印结果:

10
100
21
30 

3)原子更新引用类型
public class AtomicReferenceTest {

	public static void main(String[] args) {
		User user1 = new User("张三", 11);
		User user2 = new User("李四", 18);
		User user3 = new User("王五", 15);
		
		AtomicReference<User> atomicReference = new AtomicReference<User>();
		atomicReference.set(user1);
		System.out.println(atomicReference.get());
		
		// 都是和AtomicInteger一样 先比较user1,然后设置user2
		atomicReference.compareAndSet(user1, user2);
		System.out.println(atomicReference.get());
		
		atomicReference.compareAndSet(user1, user3);
		System.out.println(atomicReference.get());
		
	}
}
@Data
@AllArgsConstructor
// 上面两个注解需要lombok插件
class User {
	private String name;
	private Integer age;
	
}

打印结果:

User(name=张三, age=11)
User(name=李四, age=18)
User(name=李四, age=18) 

4)对象属性原子修改器
public class AtomicIntegerFieldUpdaterTest {

    public static class Candidate {
        
        volatile int score = 0;
        
        AtomicInteger salary = new AtomicInteger();
        
    }
    
    public static final AtomicIntegerFieldUpdater<Candidate> aifu = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
    
    public static AtomicInteger realScore = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        
        final Candidate candidate = new Candidate();
        
        Thread[] threads = new Thread[10000];
        
        for (int i = 0; i < 10000; i++) {
            threads[i] = new Thread(() -> {
                if (Math.random() > 0.4) {
                    candidate.salary.incrementAndGet();
                    aifu.incrementAndGet(candidate);
                    realScore.incrementAndGet();
                }
            });
            threads[i].start();
        }
        
        for (int i = 0; i < 10000; i++) {
            threads[i].join();
        }
        System.out.println("AtomicIntegerFieldUpdater Score="+candidate.score);
        System.out.println("AtomicInteger salary="+candidate.salary.get());
        System.out.println("realScore="+realScore.get());
    }
}

打印结果:

AtomicIntegerFieldUpdater Score=6057
AtomicInteger salary=6057
realScore=6057 

对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

  1. 字段必须是volatile类型的,在线程之间共享变量时保证立即可见.eg:volatile int value = 3
  2. 字段的描述类型(修饰符public/protected/default/private)与调用者与操作对象字段的 关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父 类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
  3. 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  4. 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和 volatile是有冲突的,这两个关键字不能同时存在。
  5. 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字 段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

2、LongAdder/DoubleAdder详解

解决高并发环境下AtomicInteger, AtomicLong的自旋瓶颈问题,引入了LongAdder,LongAdder类是继承Striped64类的

1)使用
/**
 * LongAdder为了解决高并发情况下自旋瓶颈
 * 原来的是单个值CAS,LongAdder是先根据base进行CAS,CAS失败再根据CPU线程数创建Cell数组,每个线程操作的是Cell数组的Cell对象value值进行累加,最后进行汇总
 * 这样就相当于开设了多个共享变量进行CAS操作
 * @author gaopu
 * @Time 2023年5月19日 下午3:39:30
 */
public class LongAdderTest {

	public static void main(String[] args) {
		// 10个线程 每个线程都自增10000次
		testAtomicLongVSLongAdder(10, 10000);
		testAtomicLongVSLongAdder(10, 200000);
		testAtomicLongVSLongAdder(100, 200000);
	}
	
	static void testAtomicLongVSLongAdder(final int threadCount, final int times) {
		try {
			long start = System.currentTimeMillis();
			testLongAdder(threadCount, times);
			long end = System.currentTimeMillis() - start;
			System.out.println("线程数:"+threadCount+",单线程操作自增次数:"+times+",LongAdder总自增次数:"+(threadCount*times)+",总耗时:"+end);
			
			long start2 = System.currentTimeMillis();
			testAtomicLong(threadCount, times);
			long end2 = System.currentTimeMillis() - start2;
			System.out.println("线程数:"+threadCount+",单线程操作自增次数:"+times+",AtomicLong总自增次数:"+(threadCount*times)+",总耗时:"+end2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	static void testAtomicLong(final int threadCount, final int times) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(threadCount);
		AtomicLong atomicLong = new AtomicLong();
		for (int i = 0; i < threadCount; i++) {
			new Thread(() -> {
				for (int j = 0; j < times; j++) {
					atomicLong.incrementAndGet();
				}
				countDownLatch.countDown();
			}, "thread"+i).start();
		}
		countDownLatch.await();
	}
	
	static void testLongAdder(final int threadCount, final int times) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(threadCount);
		LongAdder longAdder = new LongAdder();
		for (int i = 0; i < threadCount; i++) {
			new Thread(() -> {
				for (int j = 0; j < times; j++) {
					longAdder.add(1);
				}
				countDownLatch.countDown();
			}).start();
		}
		countDownLatch.await();
	}
}

打印结果:

线程数:10,单线程操作自增次数:10000,LongAdder总自增次数:100000,总耗时:59
线程数:10,单线程操作自增次数:10000,AtomicLong总自增次数:100000,总耗时:4
线程数:10,单线程操作自增次数:200000,LongAdder总自增次数:2000000,总耗时:14
线程数:10,单线程操作自增次数:200000,AtomicLong总自增次数:2000000,总耗时:40
线程数:100,单线程操作自增次数:200000,LongAdder总自增次数:20000000,总耗时:37
线程数:100,单线程操作自增次数:200000,AtomicLong总自增次数:20000000,总耗时:358
由此可见,随着线程数和自增次数增加, LongAdder的优势就体现出来了

2)分析

具体实现思想如下图:

https://www.processon.com/view/link/64c0dc6ef208ef32d3e43abd

LongAdder的sum()放存在线程不安全问题,调用sum()方法获取的结果不一定就是最终的结果 ,有可能base和Cell类的value属性正在参与运算

3)自定义计算函数
public class LongAccumulatorTest {

	public static void main(String[] args) throws InterruptedException {
        // 累加 x+y
        LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);

        ExecutorService executor = Executors.newFixedThreadPool(8);
        // 2到10累加
        IntStream.range(2, 11).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

        Thread.sleep(2000);
        System.out.println(accumulator.getThenReset());
    }
}

打印结果:54




三、并发安全问题

1、线程封闭

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对 象就算不是线程安全的也不会出现任何安全问题。

1)栈封闭

多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到 线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所 以能用局部变量就别用全局的变量,全局变量容易引起并发问题。     

2)ThreadLocal

ThreadLocal 是实现线程封闭的最好方法。ThreadLocal 内部维护了一个 Map, Map 的 key 是每个线程的名称,而 Map 的值就是我们要封闭的对象。每个线程 中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的 线程封闭。   

2、无状态的类

没有任何成员变量的类,就叫无状态的类;方法中含有其它对象导致的线程不安全,那就是方法参数中这个类的问题

3、让类不可变

让状态不可变,加 final 关键字,对于一个类,所有的成员变量应该是私有 的,同样的只要有可能,所有的成员变量应该加上 final 关键字,但是加上 final, 要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能 保证整个类是不可变的。

但是要注意,一旦类的成员变量中有对象,上述的 final 关键字保证不可变 并不能保证类的安全性,为何?因为在多线程下,虽然对象的引用不可变,但是 对象在堆上的实例是有可能被多个线程同时修改的,没有正确处理的情况下,对 象实例在堆中的数据是不可预知的。

4、加锁和CAS

我们最常使用的保证线程安全的手段,使用 synchronized 关键字,使用显式 锁,使用各种原子变量,修改数据时使用 CAS 机制等等。

5、死锁

一个锁资源肯定是不会发生死锁,最少是两个线程去争抢两个资源,争抢的顺序不对,并且抢不到就一直抢而导致死锁,在Java中可以通过jps命令,jvisualvm打开可视化界面分析什么原因导致死锁

6、活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一 个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有 的锁释放的过程(比如A、B两个线程同时进行,A线程尝试拿锁1,B线程尝试拿锁2,此时都要拿对象持有的锁资源,A线程拿不到锁2就释放了锁1,线程B拿不到锁1就释放了锁2,这样一直循环往复就永远相互等待)。

解决办法:每个线程休眠随机数,错开拿锁的时间。

7、线程饥饿

低优先级的线程,总是拿不到执行时间

8、单例模式

1)基于DCL线程安全的懒汉单例模式
public class SingletonTest {

	private SingletonTest() {}
	
	private static volatile SingletonTest singletonTest = null;
	
	public static SingletonTest get() {
		if (singletonTest == null) {
			synchronized (SingletonTest.class) {
				// 这里为什么还要判断(DCL双重检查 为了防止等待锁的线程进来没有判断又创建一个对象)
				if (singletonTest == null) {
					// java创建对象不是原子的
					// 1、申请内存空间
					// 2、对象初始化
					// 3、指向内存空间的地址
					// 要加上volatile关键字防止指令重排序返回没有初始化完的对象
					singletonTest = new SingletonTest();
				}
			}
		}
		return singletonTest;
	}
	
	public static void main(String[] args) {
		System.out.println(SingletonTest.get());
	}
}
2)虚拟机保证线程安全的单例模式
  • 懒汉单例模式
public class SingleLazy {

	private SingleLazy() {}
	
	private static class InstanceHolder {
		// 静态属性在类加载期间就初始化好了
		private static SingleLazy lazy = new SingleLazy();
	}
	
	public static SingleLazy getInstance() {
		return InstanceHolder.lazy;
	}
}
  • 饿汉单例模式
public class SingleHungry {

	private SingleHungry() {}
	private static SingleHungry hungry = new SingleHungry();
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023第五届全国生物资源提取与应用创新论坛即将举办

01、会议背景 为进一步加强生物资源提取行业交流与合作&#xff0c;促进业“产学研用”融合&#xff0c;提升行业科技创新水平&#xff0c;增强行业国际竞争力&#xff0c;中国生物发酵产业协会、浙江科技学院、浙江工业职业技术学院、浙江省农业生物资源生化制造协同创新中心&…

GFLv2 论文学习

1. 解决了什么问题&#xff1f; 预测定位质量对于目标检测很重要&#xff0c;在 NMS 时它能提供准确的得分排序&#xff0c;提高模型的表现。现有方法都是通过分类或回归的卷积特征来预测定位质量得分。 2. 提出了什么方法&#xff1f; 受到 GFLv1 的 general distribution …

Mysql 主从复制、读写分离

目录 一、前言&#xff1a; 二、主从复制原理 2.1 MySQL的复制类型 2.2 MySQL主从复制的工作过程 2.2.1 MySQL主从复制延迟 2.3 MySQL 三种数据同步方式 2.3.1、异步复制&#xff08;Async Replication&#xff09; 2.3.2、同步复制&#xff08;Sync Replication&#…

【基于CentOS 7 的iscsi服务】

目录 一、概述 1.简述 2.作用 3. iscsi 4.相关名称 二、使用步骤 - 构建iscsi服务 1.使用targetcli工具进入到iscsi服务器端管理界面 2.实现步骤 2.1 服务器端 2.2 客户端 2.2.1 安装软件 2.2.2 在认证文件中生成iqn编号 2.2.3 开启客户端服务 2.2.4 查找可用的i…

微服务远程调用openFeign简单回顾(内附源码示例)

目录 一. OpenFeign简介 二. OpenFeign原理 演示使用 provider模块 消费者模块 配置全局feign日志 示例源代码: 一. OpenFeign简介 OpenFeign是SpringCloud服务调用中间件&#xff0c;可以帮助代理服务API接口。并且可以解析SpringMVC的RequestMapping注解下的接口&#x…

在拦截器中使用redis报错空指针

问题 当在拦截器中使用 redis 时&#xff0c;获取不到 RedisTemplate 对象 原因 拦截器在SpringContext初始化之前就执行了&#xff0c;即Bean初始化之前它就执行了&#xff0c;所以肯定是无法获取SpringIOC容器中的内容的 解决 提前实例化拦截器 在配置类里面先实例化拦截…

学C的第三十天【自定义类型:结构体、枚举、联合】

相关代码gitee自取&#xff1a;C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 学C的第二十九天【字符串函数和内存函数的介绍&#xff08;二&#xff09;】_高高的胖子的博客-CSDN博客 1 . 结构体 &#xff08;1&#xff09;. 结构体的基础知识&#xff1a; 结构…

怎么学习Java网络编程? - 易智编译EaseEditing

学习Java网络编程是掌握Java语言重要的一部分&#xff0c;它使得你能够开发网络应用、客户端/服务器应用以及与远程服务进行交互。以下是学习Java网络编程的一些建议&#xff1a; 学习基本的网络概念&#xff1a; 首先&#xff0c;你需要了解计算机网络的基本概念&#xff0c…

foreverlasting and fried-chicken hdu7293

Problem - 7293 题目大意&#xff1a;给出一个n个点&#xff0c;m条边的图&#xff0c;问其中包含了几个下面这样的子图 1<n<1000; 思路&#xff1a;我们要找两个点u,v&#xff0c;他们至少有4个公共点&#xff0c;且至少有一个点的度数至少为6&#xff0c;其中还要判断…

65英寸OLED透明屏的显示效果出色吗?

65英寸OLED透明屏是一种新型的显示技术&#xff0c;它采用有机发光二极管&#xff08;OLED&#xff09;作为显示元件&#xff0c;具有高亮度、高对比度、快速响应和广视角等优点。 与传统的液晶显示屏相比&#xff0c;OLED透明屏具有更高的透明度和更好的显示效果。 OLED透明屏…

Emacs之改造最快文本搜索工具ripgrep(一百一十九)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

第三大的数

414、第三大的数 class Solution {public int thirdMax(int[] nums) {Arrays.sort(nums);int tempnums[0];int ansnums[0];int count 0;// if(nums.length<3){// return nums[nums.length-1];// }// else {for(int inums.length-1;i>0;i--){if (nums[i]>nums[i…

嵌入式_GD32看门狗配置

嵌入式_GD32独立看门狗配置与注意事项 文章目录 嵌入式_GD32独立看门狗配置与注意事项前言一、什么是独立看门狗定时器&#xff08;FWDGT&#xff09;二、独立看门狗定时器原理三、独立看门狗定时器配置过程与注意事项总结 前言 使用GD3单片机时&#xff0c;为了提供了更高的安…

Jenkins+Docker 实现一键自动化部署项目

1.安装Jenkins mkdir /docker/jenkins # 新建Jenkins工作目录 docker pull jenkins/jenkins:lts # 拉取Jenkins镜像ls -nd /docker/Jenkins # 查看目录归属ID chown -R 1000:1000 /docker/jenkins # 赋予权限注&#xff1a;因为Jenkins容器里的用户是Jenkins&#xff0c;…

C# Modbus TCP上位机测试

前面说了三菱和西门子PLC的上位机通信&#xff0c;实际在生产应用中&#xff0c;设备会有很多不同的厂家生产的PLC&#xff0c;那么&#xff0c;我们就需要一种通用的语言&#xff0c;进行设备之间的通信&#xff0c;工业上较为广泛使用的语言之一就是Modbus。 Modbus有多种连…

2023年基准Kubernetes报告:6个K8s可靠性失误

云计算日益成为组织构建应用程序和服务的首选目的地。尽管一年来经济不确定性的头条新闻主要集中在通货膨胀增长和银行动荡方面&#xff0c;但大多数组织预计今年的云使用和支出将与计划的相同&#xff08;45%&#xff09;&#xff0c;或高于计划的&#xff08;45%&#xff09;…

MIT 6.830数据库系统 -- lab four

MIT 6.830数据库系统 -- lab four 项目拉取引言事务、锁 & 并发控制事务ACID特性两阶段锁 Recovery and Buffer ManagementGranting Locks(授予锁)练习1 Lock Lifetime练习2 Implementing NO STEAL练习3 事务练习4 死锁和中止练习5 项目拉取 原项目使用ant进行项目构建&am…

微服务系列(1)-who i am?

微服务系列&#xff08;1&#xff09;-我是谁 应用架构的演化 简单来说系统架构可以分为以下几个阶段&#xff1a;复杂的臃肿的单体架构-SOA架构-微服务 单体架构及其所面临的问题 在互联网发展初期&#xff0c;用户数量少&#xff0c;流量小&#xff0c;硬件成本高。因此…

96、Kafka中Zookeeper的作用

Kafka中zk的作用 它是一个分布式协调框架。很好的将消息生产、消息存储、消息消费的过程结合在一起。在典型的Kafka集群中, Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用…

leetcode做题笔记37

编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; 数独部分…
最新文章