并发无锁工具类——原子类

1. 原子类的简单运用和原理

首先回顾一下经典的累加器的案例:

class My {
    public int count = 0;
    public void run() {
        for(int i=0; i<10000; i++) {
            count++;
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws Exception{
        My my = new My();
        for(int i=0; i<10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    my.run();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(my.count);
    }
}

测试输出的结果一般是小于100000的,因为count存在可见性和原子性的问题(指count++存在原子性问题),所以会出现以上结果;

以往的解决方案就是把run方法那一块代码加上锁,就保证了线程安全;
**那么只将count变量用volatile修饰能保证正确结果吗?**答案是不会的,volatile只保证了可见性,并没有保证原子性,所以结果同样会小于100000;

1. 用原子类来解决简单的原子性问题

对于上面的案例,就是一个简单的原子性问题,此时我们可以借助juc包下的原子类来解决,代码如下:

class My {
    AtomicInteger count = new AtomicInteger(0);
    public void run() {
        for(int i=0; i<10000; i++) {
            count.getAndIncrement();
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws Exception{
        My my = new My();
        for(int i=0; i<10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    my.run();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(my.count);  //一定是100000
    }
}

上面我们使用的AtomicInteger就是一个原子类(JDK5提供),原子类是无锁方案实现的,所以上面的代码比我们使用锁的代码的效率要高得多;具体体现在:

  • 互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而这两个操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换也会带来性能的消耗;所以相比之下,无锁方案性能是远远高于它的;

2. 无锁方案的实现原理

上面简单使用了原子类,在保证线程安全的情况下还性能大大优于互斥锁方案,那原子类到底是如何实现的呢?

原子类的实现原理其实就是CAS操作; ⭐ ⭐(CAS操作是乐观锁的一个实现方式!

  • CAS⽐较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O预期的值(旧值);N 更新的新值;
    • 当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是⽬前来说最新的值了,⾃然⽽然可以将新值N赋值给V;
    • 反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使⽤CAS操作⼀个变量时,只有⼀个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程;

下面通过一段代码来进行模拟实现CAS操作:

class MyCAS {
    int count;
    synchronized int cas(int O, int N) {
        //获取内存中真实的值
        int V = count;
        //如果期待的值与内存中的值一致,则代表期间没有其他线程修改过这个值;
        if(O == V) {
            count = N;
        }
        //返回修改前的值
        return V;
    }
}

那么获取你会问,上面那个synchronized不就是锁吗?不是叫无锁操作吗?

  • 注意啦,那个锁是我们硬件所提供支持的,也就是说每个CAS操作都是由硬件首先保证是互斥的,整个CAS操作是硬件提供的一组指令集,这个是保证原子性的,我们在使用其他的方法就是在调用这个cas方法的基础上,比如上面的getAndIncrement()方法,这个方法肯定是无锁的,但这个方法里面使用了cas方法,相当于cas方法是系统提供的;
    CAS的实现需要硬件指令集的⽀撑,在JDK1.5后虚拟机才可以使⽤处理器提供的CMPXCHG指令实现

3. 案例具体细节解释

那么上面展示了原子类的实现原理,到底是怎样来保证线程安全的呢?

使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。例如,实现一个线程安全的count += 1操作,“CAS+ 自旋”的实现方案如下所示,首先计算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着线程在执行完代码①处之后,执行代码②处之前,count 的值被其他线程更新过。那此时该怎么处理呢?可以采用自旋方案,就像下面代码中展示的,可以重新读 count 最新的值来计算 newValue 并尝试再次更新,直到成功;

class MyCAS {
    int count;
    public void addOne() {
        int newValue;
        do {
            newValue = count + 1;  //①
        }while (count != 
                cas(count, newValue)  //②
               );
    }
    synchronized int cas(int O, int N) {
        //获取内存中真实的值
        int V = count;
        //如果期待的值与内存中的值一致,则代表期间没有其他线程修改过这个值;
        if(O == V) {
            count = N;
        }
        //返回修改前的值
        return V;
    }
}

上面如果有点绕的话,这里来一波细节: ⭐⭐
假如有两个线程A、B来同时执行addOne()方法,此时假设count为5,此时A、B都执行到了while语句,括号里面读到的count都是5,接下来两个线程都要去执行cas方法,由于这个方法是加锁的,所以只能有一个线程能够进入,假如A线程进入了,来到cas方法内部,读取到V值为5,接下来判断o==v是成立的,所以将count更新为了6,然后返回了5,此时线程A就将addOne方法执行结束了,而此时B才获取到锁,进入了cas方法内部,此时获取到了V为6,if语句不成立,返回6,此时线程B在while那判断5 != 6,所以再次循环一次,操作跟上面一样,最终count变为7;

4. Java中实现CAS的源码⭐

在上面的案例中,我们用AtomicInteger来实现了线程安全的count++操作,这个操作内部具体是怎么实现的呢,下面来看看源码:

	public final int getAndIncrement() {
	//this和valueoffset可以唯一确定共享变量的内存地址,参数1代表加一;
    	return unsafe.getAndAddInt(this, valueOffset, 1);
	}
	
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
        	//获取当前内存中的值
            var5 = this.getIntVolatile(var1, var2);
            //传入va1,var是为了在下面这个CAS方法中再次获取内存中的真实值,var5就是期待的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
        
//更新成功则返回true,条件是内存中的值等于期待的值,即上面传进来的var5要等于待会在这个方法里面根据算出的
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

上面这个模型跟3中总结的模型其实是一样的,这里的compareAndSwapInt其实就是CAS,内部操作就是比较期待的值和内存值是否一样,一样就更新内存,返回true,否则不更新内存,返回false,此时回到getAndIncrement中,继续循环,此时新的循环会读取最新内存中的值,继续执行下去!!

2. 原子类概览

在这里插入图片描述

在juc包下,有一个包为atomic包,其下提供了很多丰富的原子类,我们可以将他们分为五个类别:

  • 原子化的基本数据类型
  • 原子化的对象引用类型
  • 原子化数组
  • 原子化对象属性更新器
  • 原子化的累加器

JDK从1.5开始提供了java.util.concurrent.atomic包;
作用:通过包中的原子操作类能够线程安全地更新一个变量;
包含4种类型的原子更新方式:基本类型、数组、引用、对象中字段更新;

1. 原子化的基本数据类型

这一类包含的类有:(这些类都是juc.atomic包下的类

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

这三个类的实现和作用都很相似,来看看他们的方法就行了(他们提供的方法都大致一样)

getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta) 
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四个方法
// 新值可以通过传入 func 函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

2. 原子化的对象引用类型

在juc包下的atomic包下有如下实现:

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

利用这几个类可以实现对象引用的原子化更新;

对象引用的更新需要重要关注ABA问题,AtomicMarkableReferenceAtomicStampedReference 这两个类可以解决ABA问题,其解决方案就是在每次更新中添加一个版本号;

具体使用可以看下面这个案例:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "[name: " + this.name + ", age: " + this.age + "]";
    }
}
public class Test2 {
    // 普通引用
    private static Person person;

    public static void main(String[] args) throws InterruptedException {
        person = new Person("Tom", 18);

        System.out.println("Person is " + person.toString());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Now Person is " + person.toString());
    }

    static class Task1 implements Runnable {
        public void run() {
            person.setAge(person.getAge() + 1);
            person.setName("Tom1");

            System.out.println("Thread1 Values :"
                    + person.toString());
        }
    }

    static class Task2 implements Runnable {
        public void run() {
            person.setAge(person.getAge() + 2);
            person.setName("Tom2");

            System.out.println("Thread2 Values :"
                    + person.toString());
        }
    }
}

可能输出:
Person is [name: Tom, age: 18]
Thread2 Values [name: Tom1, age: 21]
Thread1 Values [name: Tom1, age: 21]
Now Person is [name: Tom1, age: 21]

假如我们使用了原子类的对象引用,就不会出现上述情况:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "[name: " + this.name + ", age: " + this.age + "]";
    }
}
public class Test2 {
    // 原子引用
    private static AtomicReference<Person> personAtomicReference;
    public static void main(String[] args) throws InterruptedException {
        Person person = new Person("Tom", 18);
        personAtomicReference = new AtomicReference<Person>(person);

        System.out.println("Person is " + person.toString());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Now Person is " + person.toString());
    }

    static class Task1 implements Runnable {
        public void run() {
            personAtomicReference.getAndSet(new Person("Tom1", personAtomicReference.get().getAge() + 1));

            System.out.println("Thread1 Atomic References "
                    + personAtomicReference.get().toString());
        }
    }

    static class Task2 implements Runnable {
        public void run() {
            personAtomicReference.getAndSet(new Person("Tom2", personAtomicReference.get().getAge() + 2));

            System.out.println("Thread2 Atomic References "
                    + personAtomicReference.get().toString());
        }
    }
}

输出:
Person is [name: Tom, age: 18]
Thread1 Atomic References [name: Tom1, age: 19]
Thread2 Atomic References [name: Tom2, age: 21]
Now Person is [name: Tom, age: 18]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章