高并发多线程安全之原子性问题cas机制及问题解决方案(代码片段)

踩踩踩从踩 踩踩踩从踩     2023-01-04     597

关键词:

多线程编程 之java内存模型(JMM)与可见性问题

前言

在java内存模型中,对多线程间交互,涉及到原子性问题、可见性问题、以及有序性问题;

这篇文章主要讲解的是多线程高并发的原子性问题,以及解决原子性问题、CAS机制、自旋锁的优缺点、以及ABA问题等解决

什么是原子操作

定义

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

通过下面代码来看出来

public class Counter 
    volatile int i = 0;

    public void add() 
        i = i + +;
    
public class Demo1_CounterTest 

	public static void main(String[] args) throws InterruptedException 
		final Counter ct = new Counter();
		CountDownLatch cl = new CountDownLatch(6);
		for (int i = 0; i < 6; i++) 
			new Thread(new Runnable() 
				@Override
				public void run() 
					for (int j = 0; j < 10000; j++) 
						ct.add();
					
					cl.countDown();
					System.out.println("done...");
				
			).start();
		
		cl.await();
		System.out.println(ct.i);
	

上面得到的结果完全不可控,得到了下面的打印结果;明显产生了冲突,在预想的时候应该产生60000,但只得到了34684;多执行一次结果值又不一样

done...
done...
done...
done...
done...
done...
34684

产生情况的原因,为什么会出现值减少的情况

  • i++不是原子操作,这在多线程操作时会出现非原子性行为

 

  •  这是在每个线程中的私有方法内存中进行操作,就可能导致冲突,出现非原子性行为;但不是可见性问题,因此我们写入内存时,其他线程一定能看到,通过 volatile 已经发生happens-before原则

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也 不可以被切割 而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体, 资源在该次操作中保持一致 ,这是原子性的核心特征

结论:

i++不是原子操作,存在竞态条件,线程不安全,需要转变为原子操作才能安全。要保证数据安全
public synchronized void add() 
		i = i + 1;
	

竞态条件与临界区

 竞态条件:两个线程竞争同一个资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

临界区:就是导致静态条件发生的代码区。 比如上图的 i++;代码

CAS(Compare and swap) 机制

public class CounterAtomic 
    AtomicInteger at = new AtomicInteger(0);

    public void add()
        at.getAndIncrement();
    

    public int getValue()
        return at.get();
    

这里用到的AtomicInteger  就是利用的cas机制来达到原子性的

  • CAS 属于 硬件 同步原语,处理器提供的内存操作指令, 保证原子性
  • CAS操作需要两个参数,一个旧值和目标值,修改前比较旧值是否改变,如果没变,将新值赋给变量,否则则不做改变。

如下图,去对比值,如果没变就赋值为111

 如果两个线程同时进行操作,对于内存来说,其实还是分为两次操作,它保证同一时刻只能有一个进行修改

修改值之前进行检查,如果值是正常,就修改成功,修改失败就在更新老值进行重试。

 CAS 属于硬件同步原语,处理器提供的内存操作指令,而jvm虚拟机提供给一个可以使用的类unsafe;

JAVA中的sun.misc.Unsafe类提供了CAS机制。

例如AtomicInteger中使用到的

 // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static 
        try 
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
         catch (Exception ex)  throw new Error(ex); 
    

    private volatile int value;

这个可以在自己仿造着AtomicInteger 写一个unsafe的原子操作类

	static 
		unsafe = Unsafe.getUnsafe();
		try 
			valueOffset = unsafe.objectFieldOffset(CountUnsafe.class.getDeclaredField("value"));
		 catch (Exception ex) 
			throw new Error(ex);
		
	

 在写到上面代码时,出现了安全异常,而automicInteger中则没有异常,jdk不允许我们拿。然后就使用反射去解决

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at org.cao.learn.CountUnsafe.<clinit>(CountUnsafe.java:12)
  •  按照automicinteger可以写一个,先定义属性
private static final Unsafe unsafe;
	private static final long valueOffset;

	private volatile int value = 0;
  • 定义好静态方法块
static 
//		unsafe = Unsafe.getUnsafe();

		try 
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			valueOffset = unsafe.objectFieldOffset(CountUnsafe.class.getDeclaredField("value"));
		 catch (Exception ex) 
			throw new Error(ex);
		
	
  • 然后总的代码  
package org.cao.learn;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class CountUnsafe 
	private static final Unsafe unsafe;
	private static final long valueOffset;

	private volatile int value = 0;

	static 
//		unsafe = Unsafe.getUnsafe();

		try 
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			unsafe = (Unsafe) field.get(null);
			valueOffset = unsafe.objectFieldOffset(CountUnsafe.class.getDeclaredField("value"));
		 catch (Exception ex) 
			throw new Error(ex);
		
	

	public final int getAndIncrement() 
		return unsafe.getAndAddInt(this, valueOffset, 1);
	

	public void add() 
		// i++; 
		for (;;) 
			if (unsafe.compareAndSwapInt(this, valueOffset, value, value + 1)) 
				return;
			
		
	

	public int getValue() 
		return value;
	


这里的getAndIncrement方法和add方法是一样的,最好是使用getAndAddInt 方法 这是native方法做了内循环,并做了优化的.

J.U.C包内的原子操作封装类

  • AtomicBoolean原子更新布尔类型
  • AtomicInteger原子更新整型
  • AtomicLong 原子更新长整型
  • AtomicIntegerArray 原子更新整型数组里的元素。
  public AtomicIntegerArray(int[] array) 
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    

底层存的int类型,需要对其中一个元素做原子性操作

    public final int getAndSet(int i, int newValue) 
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    
  • AtomicLongArray 原子更新长整型数组里的元素。
  • AtomicReferenceArray原子更新引用类型数组里的元素。
  • AtomicIntegerFieldUpdater原子更新整型的字段的更新器。

这里做的引用就是修改对象的某个值,是它为原子操作。

ublic class Demo2_AtomicIntegerFieldUpdater 
    // 新建AtomicIntegerFieldUpdater对象,需要指明是哪个类中的哪个字段


    private static AtomicIntegerFieldUpdater<User> atom =
            AtomicIntegerFieldUpdater.newUpdater(User.class, "id");

    public static void main(String[] args) 

        User user = new User(100, 100,"Kody");

        atom.addAndGet(user, 50);
        System.out.println("addAndGet(user, 50)             调用后值变为:" + user);

    


class User 
    volatile int id;
    volatile int age;

    private String name;

    public User(int id, int age, String name) 
        this.id = id;
        this.age = age;
        this.name = name;
    

    public String toString() 
        return "id:" + id + " " + "age:" + age;
    
  • AtomicLongFieldUpdater原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater原子更新引用类型里的字段
  • AtomicReference原子更新引用类型。

       这里也是对比对象应用,包括下面的锁机制的一部分抢锁时,对比的是锁引用是否相等。

 public boolean tryLock() 
        //如果锁没有占用     存在原子性问题
    
        return owner.compareAndSet(null, Thread.currentThread());
    
  • AtomicStampedReference原子更新带有版本号的引用类型
  • AtomicMarkableReference原子更新带有标记位的引用类型。
1.8更新
计数器: DoubleAdder LongAdder
计数器增强版,高并发下性能更好 ,多个道路进行累加,然后进行相加
public long testLongAdder() throws InterruptedException 
        LongAdder lacount = new LongAdder();
        for (int i = 0; i < 3; i++) 
            new Thread(() -> 
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000)  // 运行两秒
                    lacount.increment();
                
                long endtime = System.currentTimeMillis();
            ).start();
        
        Thread.sleep(3000);
        return lacount.sum();
    

DoubleAccumulator、 LongAccumulator
是计数器的增强版,可自定义累加规则   可以指定逻辑规则, 做一个增强版操作。自定义规则,x和y值 其中0为x ,y为每次规则过后结果
public class Demo3_LongAccumulator 
    public static void main(String[] args) throws InterruptedException 

        LongAccumulator accumulator = new LongAccumulator(
                (x,y)->
                    System.out.println("x:" + x);
                    System.out.println("y:" + y);
                    return  x+y;
                ,
                0L);

        for (int i = 0; i < 3; i++) 
            accumulator.accumulate(1);
        

        System.out.println(accumulator.get());

    

CAS存在的问题

  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作
  • 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会消耗大量的cpu资源。

  • ABA问题,无法体现出数据的变动。

主要是多线程操作时,其中一个线程操作过后,得到的新值

上述的方式,在automicinteger里面不出问题,在AtomicReference中可能出现问题,也是链表结构的情况

public class Stack 
    // top cas无锁修改
    AtomicReference<Node> top = new AtomicReference<Node>();

    public void push(Node node)  // 入栈
        Node oldTop;
        do 
            oldTop = top.get();
            node.next = oldTop;
        
        while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
    


    // 出栈
    public Node pop(int time) 

        Node newTop;
        Node oldTop;
        do 
            oldTop = top.get();
            if (oldTop == null)    //如果没有值,就返回null
                return null;
            
            newTop = oldTop.next;
            if (time != 0)     
                LockSupport.parkNanos(1000 * 1000 * time); 
            
        
        while (!top.compareAndSet(oldTop, newTop));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    

 测试方法

 public static void main(String[] args) throws InterruptedException 
        Stack stack = new Stack();
        //ConcurrentStack stack = new ConcurrentStack();

        stack.push(new Node("B"));      
        stack.push(new Node("A"));      

        Thread thread1 = new Thread(() -> 
            Node node = stack.pop(800);
            System.out.println(Thread.currentThread().getName() +" "+ node.toString());

            System.out.println("done...");
        );
        thread1.start();

        Thread thread2 = new Thread(() -> 
            LockSupport.parkNanos(1000 * 1000 * 300L);

            Node nodeA = stack.pop(0);      
            System.out.println(Thread.currentThread().getName()  +" "+  nodeA.toString());

            Node nodeB = stack.pop(0);      //取出B,之后B处于游离状态
            System.out.println(Thread.currentThread().getName()  +" "+  nodeB.toString());

            stack.push(new Node("D"));      
            stack.push(new Node("C"));      
            stack.push(nodeA);                    

            System.out.println("done...");
        );
        thread2.start();

        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);


        System.out.println("开始遍历Stack:");
        Node node = null;
        while ((node = stack.pop(0))!=null)
            System.out.println(node.value);
        
    

 

线程1在拿到 老的top值,和新的node值时过后,进行cas比较并赋值的情况等待状态了;然后线程2中间插入了很多数据,又插回了原来那个对象,因此出现了ABA问题。线程2put进去的数据都变不在了,其实里面属性已经变了

解决ABA问题的办法

主要是我们不能将top引用作为对比的对象,如果按照引用去对比,属性修改,数据肯定不正确了。

AtomicStampedReference<Node> top =
            new AtomicStampedReference<>(null, 0);

AtomicStampedReference添加了版本号,不只是对比引用,加入了版本号维护;

package org.cao.learn;

import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.LockSupport;

// 实现一个 栈(后进先出)
public class Stack 
	// top cas无锁修改
	AtomicStampedReference<Node> top = new AtomicStampedReference<Node>(null, 0);
//	AtomicReference<Node> top = new AtomicReference<Node>();

	public void push(Node node)  // 入栈
		Node oldTop;
		int v = 0;
		do 
			v = top.getStamp();
			oldTop = top.getReference();
			node.next = oldTop;
		 while (!top.compareAndSet(oldTop, node, v, v + 1)); // CAS 替换栈顶
	

	// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
	public Node pop(int time) 

		Node newTop;
		Node oldTop;
		int v = 0;
		do 
			oldTop = top.getReference();
			v = top.getStamp();
			if (oldTop == null)  // 如果没有值,就返回null
				return null;
			
			newTop = oldTop.next;
			if (time != 0)  // 模拟延时
				LockSupport.parkNanos(1000 * 1000 * time); 
			
		 while (!top.compareAndSet(oldTop, newTop, v, v + 1)); 
		return oldTop; 
	

多线程cas机制解析及应用(原子类.自旋锁)解决aba问题(代码片段)

@TOCCAS(Compareandswap)1、解析CASCAS:全称Compareandswap,字面意思:”比较并交换“,一个CAS涉及到以下操作:CAS伪代码:下面写的代码不是原子的,真实的CAS是一个原子的硬件指令完成的.这个伪代码只是辅助理解CAS的工作流程.booleanCAS... 查看详情

多线程之cas

...同步的,这会导致有锁锁机制存在以下问题:(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。(3)如果一个优先级高的线... 查看详情

java并发之cas详解(代码片段)

前言在高并发的应用当中,最关键的问题就是对共享变量的安全访问,通常我们都是通过加锁的方式,比如说synchronized、Lock来保证原子性,或者在某些应用当中,用voliate来保证变量的可见性,还有就是通过TheadLocal将变量copy一... 查看详情

并发编程(学习笔记-共享模型之无锁)-part5(代码片段)

文章目录并发编程-5-共享模型之无锁1.无锁解决线程安全问题2.CAS与volatile2-1CAS2-2volatile2-3为什么无锁效率高2-4CAS特点3.原子整数4.原子引用4-1原子引用的使用4-2ABA问题及解决5.原子数组6.字段更新器7.原子累加器8.LongAdder详解8-1cas锁8... 查看详情

java——多线程高并发系列之理解cas原子变量类的使用(代码片段)

文章目录:1.CAS2.原子变量类2.1AtomicInteger2.2AtomicLong2.3AtomicIntegerArray2.4AtomicIntegerFieldUpdater2.5AtomicReference2.6AtomicStampedReference1.CASCAS(CompareAndSwap)是由硬件实现的。CAS可以将read-modify-write 查看详情

java并发编程实战之互斥锁(代码片段)

文章目录Java并发编程实战之互斥锁如何解决原子性问题?锁模型Javasynchronized关键字Javasynchronized关键字只能解决原子性问题?如何正确使用Javasynchronized关键字?锁和受保护资源的合理关联关系死锁预防死锁破坏占有... 查看详情

java并发编程实战之互斥锁(代码片段)

文章目录Java并发编程实战之互斥锁如何解决原子性问题?锁模型Javasynchronized关键字Javasynchronized关键字只能解决原子性问题?如何正确使用Javasynchronized关键字?锁和受保护资源的合理关联关系死锁预防死锁破坏占有... 查看详情

java程序中怎么保证多线程的运行安全?

并发编程三要素(线程的安全性问题体现在):原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。... 查看详情

java并发编程线程锁机制(悲观锁|乐观锁|cas三大问题|aba问题|循环时间长问题|多个共享变量原子性问题)(代码片段)

文章目录一、悲观锁二、乐观锁三、乐观锁CAS三大问题一、悲观锁假设有222个线程,线程A和线程B;线程A访问共享资源,线程B等待,一旦线程A访问结束,线程B访问该共享资源;悲观锁:只要有线程来操作共享资源,就认为肯定有其它若干... 查看详情

多线程&高并发深入理解jmm产生的三大问题原子性可见性有序性

【多线程&高并发】深入浅出原子性https://blog.csdn.net/Kevinnsm/article/details/121681785?spm=1001.2014.3001.5501【多线程&高并发】深入浅出可见性https://blog.csdn.net/Kevinnsm/article/details/121683823?spm=1001.2014.3001.5501【多线程&高并发】jcs... 查看详情

java核心-多线程-并发原子类

????使用锁能解决并发时线程安全性,但锁的代价比较大,而且降低性能。有些时候可以使用原子类(juc-atomic包中的原子类)。还有一些其他的非加锁式并发处理方式,我写这篇文章来源于Java中有哪些无锁技术来解决并发问题的... 查看详情

提升--05---并发编程之---原子性---cas(代码片段)

并发编程之原子性从一个简单的小程序谈起:需求:new100个线程,每个线程对共有资源n,进行+1操作,每个线程执行10000次.按需求,最后共有资源n,应该是100*10000=1000000(1百万)代码1:importjava.util.concurrent.CountDownLatch;publicclassT00_00_... 查看详情

java并发多线程编程——原子类atomicinteger的aba问题及原子更新引用(代码片段)

目录一、ABA问题的概述二、ABA问题的产生代码示例三、原子引用类(AtomicReference)3.1、jdk1.8API中的原子引用类截图如下:3.2、原子引用类代码示例四、ABA问题的解决4.1、ABA问题的解决思路3.2、解决ABA问题的代码示例一... 查看详情

java开发之高并发必备篇——线程安全操作之synchronized

提到并发编程大多数第一时刻想到的就是synchronized同步锁了,synchronized也是面试中问的比较多的一个问题。在之前的文章中我们提到过线程安全的三个特性:原子性、可见性和有序性,并且说到了java中定义了一个关... 查看详情

java开发之高并发必备篇——线程安全操作之synchronized

提到并发编程大多数第一时刻想到的就是synchronized同步锁了,synchronized也是面试中问的比较多的一个问题。在之前的文章中我们提到过线程安全的三个特性:原子性、可见性和有序性,并且说到了java中定义了一个关... 查看详情

高并发编程-06-可见性-volatile

1,volatile的作用volatile是一个轻量级的线程同步机制。它的特性之一,是保证了变量在线程之间的可见性。当然,还有我们之前说的,解决指令重排的问题volatile保证了在多个线程之间是可见的,但不能保证原子性操作当一个线... 查看详情

cas,在硬件层面为并发安全保驾护航(代码片段)

文章目录一、前言二、CAS操作2.1AtomInteger类保证原子性2.2CAS三步操作2.3AtomicInteger类使用for循环+if(cas)来保证原子性三、CAS的ABA问题3.1ABA问题定义3.2ABA问题重现3.3ABA问题解决3.4代码解释ABA问题两个类处理3.4.1AtomicStampedReference类代... 查看详情

java多线程编程——对象及变量的并发访问(代码片段)

Java多线程编程——对象及变量的并发访问文章目录Java多线程编程——对象及变量的并发访问前言一、synchronized同步方法1.方法内的变量为线程安全2.实例变量非线程安全问题与解决方案3.同步synchronized在字节码指令中的原理4.多... 查看详情