Java原子操作和CAS

为了开发者更方便的编写多线程程序,Java提供了一些原子操作类,在java.util.concurrent.atomic包下。

什么是原子操作?

操作:实现特定功能的1行或N行代码,或一个方法。

原子操作:多线程下,某个线程在执行该操作时,不允许被其他线程打断。

程序运行时,CPU在多个线程中快速的切换,每个线程在运行下一条指令前,都有可能失去CPU的执行权。

对于一个方法或代码块,如果不做任何处理,那么它肯定不是原子操作。

如何保证非原子操作的安全性?

  • synchronized
  • 加显式锁

但是这两种方式,开销都太大了,

synchronized:基于阻塞的锁机制

  • 被阻塞的线程优先级很高
  • 热点资源,大量锁竞争,消耗CPU
  • 产生死锁,以及其他线程安全问题。

对于一些简单的操作,使用synchronized或者显式锁开销太大了,为了解决这个问题,Java提供了原子操作类

原子操作类

java.util.concurrent.atomic包下提供了4种类型的操作类:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新属性

常用API

  • V get()
    获取值

  • void set(V v)
    赋值

  • void lazySet(V v)
    懒赋值,不能保证对其他线程的“可见性

  • V getAndSet(V v)
    返回旧值,并赋新值

  • boolean compareAndSet(V v1, V v2)
    比较并替换,返回是否替换成功

简单例子

public class AtomicBooleanDemo {
	AtomicInteger ai = new AtomicInteger(0);
	int i = 0;

	void add(){
		//休眠1ms 结果更明显
		SleepUtil.sleep(1);
		//原子操作
		System.out.println(ai.incrementAndGet());
		//非原子操作
		System.out.println(++i);
	}

	public static void main(String[] args) {
		AtomicBooleanDemo demo = new AtomicBooleanDemo();
		for (int i = 0; i < 1000; i++) {
			new Thread(()->{
				demo.add();
			}).start();
		}
	}
}

开启1000个线程去对i和ai加1,++i不是原子操作,输出结果最大值小于1000,incrementAndGet()是原子操作,输出结果最大值等于1000。

CAS

什么是CAS?

CAS:Compare and Swap,译为:比较并交换

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

如果V和A相等,那么就用B来替换。

用Java代码描述如下:

public class CASDemo {
    int V;
    boolean compareAndSet(int A, int B) {
        if (V == A) {
            V = B;
            return true;
        }
        return false;
    }
}

这里的compareAndSet肯定是非原子操作。

CAS可以做到:在不加锁的情况下,确保“比较和替换”操作是原子的。

CAS操作是原子的,但不是锁。
它是利用现代处理器中都支持的原子指令来实现的,Java本身不能实现,需要调用本地方法,利用C语言/汇编来实现。

对于简单的“比较和替换”操作,就不要去使用锁,开销太大了,使用CAS性能更高。

存在的问题

  • ABA问题
    通过版本号解决

  • 开销问题

  • 只能保证一个共享变量的原子操作
    使用AtomicReference解决。

解决ABA问题

ABA问题:假设初始值为A,线程1首先将A改为B,线程2又将B改为A,线程3无法判断A是否被修改过。

Java提供了两个类来解决ABA问题:

  • AtomicMarkableReference
    记录是否被修改过。

  • AtomicStampedReference
    记录每次修改的版本号。

AtomicStampedReference除了比较期望值和原值是否相等外,还会判断版本号是否一致,只有都一致,才会进行修改。

AtomicStampedReference例子

public class AtomicStampedReferenceDemo {
	//构建AtomicStampedReference实例 并设置:初始值和初始版本号
	AtomicStampedReference<String> asr = new AtomicStampedReference("A", 0);

	public static void main(String[] args) {
		AtomicStampedReferenceDemo demo = new AtomicStampedReferenceDemo();
		//初始版本号
		int defaultStamp = demo.asr.getStamp();
		//初始值
		String defaultValue = demo.asr.getReference();
		//第一次修改 A > B 版本号 0 > 1 true
		boolean oneChange = demo.asr.compareAndSet(defaultValue, "B", defaultStamp, defaultStamp+1);
		//第二次修改 B > A 版本号 1 > 2 true
		boolean twoChange = demo.asr.compareAndSet("B", defaultValue, defaultStamp+1, defaultStamp+2);
		//第三次修改 A > B 版本号 0 > 1 false
		boolean threeChange = demo.asr.compareAndSet(defaultValue, "B", defaultStamp, defaultStamp + 1);
	}
}

解决只能一个共享变量的问题

  • AtomicReference

AtomicReference使用和原子基本类型相似,区别是AtomicReference保存和比较的是对象引用的内存地址。

public class AtomicReferenceDemo {

	private static class Person{
		String name;
		int age;
	}

	public static void main(String[] args) {
		Person person = new Person();
		person.name = "admin";
		person.age = 18;

		AtomicReference<Person> ar = new AtomicReference<>();
		ar.set(person);

		Person newPerson = new Person();
		newPerson.name = "Lisa";
		newPerson.age = 23;

		//替换成功 ar内部指向newPerson
		ar.compareAndSet(person, newPerson);

		Person arPerson = ar.get();
		System.out.println(arPerson == newPerson);//true
		System.out.println(arPerson.name);//Lisa
		System.out.println(person.name);//admin
	}
}
发布了100 篇原创文章 · 获赞 23 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_32099833/article/details/103189166