【多线程与并发】如果不用锁机制如何实现共享数据访问

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Soldier49Zed/article/details/101356102

前提:不能用锁,不能用synchronize块或者方法,也不能直接使用jdk提供的线程安全的数据结构,需要自己实现一个类来保证多个线程同时读写这个类中的共享数据是线程安全的。

无锁化编程的常用方法:硬件CPU同步原语CAS(Compare And Swap),如无锁栈、无锁队列(ConcurrentLinkedQueue)等待。现在几乎所有的CPU指令都支持CAS的院子操作,X86下对应的是CMPXCHG汇编指令,处理器执行CMPXCHG指令是一个原子性操作。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构。

CAS实现了区别于synchronized同步锁的一种乐观锁,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有三个操作数,内存值V、旧的预期值A,要修改后的新值B。当且仅当预期值A和内存值V相同时,将内存值修改为B,否则什么都不做。其实CAS也算是有锁操作,只不过是由CPU来触发,比如synchronized性能好的多。CAS的关键点在于,系统在硬件层面保证了比较并交换操作的原子性,处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的院子操作。CAS是非阻塞算法的一种常见实现。

一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从工作内存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用CAS刷新该值的时候,如果发现线程工作内存和主存不一致了,就会失败,如果一致,就可以更新成功。

Atomic包提供了一系列原子类。这些类可以保证多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。

AtomicInteger是一个支持原子操作的Integer类,就是保证对AtomicInteger类型变量的增加或减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用AtomicInteger,要实现一个按顺序获取的ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的ID的现象。Java并发库中的AtomicXXX类均是基于这个原语的实现,拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的:来看看++i是怎么做到的

public final int incrementAndGet(){
    for(;;){
        int current = get();
        int next = current + 1;
        if(compareAndSet(current,next))
            return next;
    }
}

在这里才用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重启知道成功为止

而compareAndSet利用了JNI来完成CPU指令的操作,非阻塞算法。

public final boolean compareAndSet(int expect,int update){
    return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}

其中,unsafe.compareAndSwapInt()是一个native方法,正是调用CAS原语完成该操作的。

首先假设有一个变量i,i的初始值为0.每个线程都对i进行一次+1操作。CAS是这样保证同步的:

假设有两个线程,线程1读取内存中的值为0,current = 0,next = 1,然后挂起,然后线程2对i进行操作,将i的值变成了1。线程2执行完,回到线程1,进入if里面的compareAndSet()方法,该方法进行的操作逻辑是

  1. 如果操作数的值在内存中没有被修改,返回true,然后compareAndSet方法返回next的值
  2. 如果操作数的值在内存中被修改了,则返回false,重新进入下一次循环,重新得到current的值为1,next的值为2,然后在比较,由于这次没有被修改,所以直接返回2。

那么,为什么自增操作要通过CAS来完成呢?仔细观察incrementAndGet()方法,发现自增操作时拆成了两部分完成的。

int current = get();
int next = current + 1;

由于volatile只能保证读取或写入的是最新值,那么可能出现以下情况:

  1. A线程执行get()操作,获取current值(假设为1)
  2. B线程执行get()操作,获取current值(为1)
  3. B线程执行next = current + 1操作,next = 2
  4. A线程执行next = current = 1操作,next = 2

这样的结果明显不是我们想要的,所以自增操作必须采用CAS来完成。

CAS的优缺点

CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。

CAS虽然很高效的实现了院子操作,但是它依然存在三个问题。

一、ABA问题 

CAS在操作值的时候和检查值是否已经变化,没有变化的情况下才会进行更新。但是如果一个值原来是A,变成B,又变成A,那么CAS进行检查时会认为这个值没有变化,操作成功。ABA问题的解决方式是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决问题。从这个类的compareAndSet方法作用首先是检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以院子方式将该引用和该标志的值设置为给定的更新值。

CAS算法实现了一个重要前提是需要取出内存中某时刻的数据,而在下一时刻把取出后的数据和内存中原始数据比较并替换,那么在这个时间差内会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
head.compareAndSet(A,B);
在T1执行上面这条指令前,线程T2介入,将A,B出栈,在Push D、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:


其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本号version来对记录或对象标记,避免并生操作带来的问题。在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元祖来对对象标记版本戳stamp,从而避免CAS问题

二、循环时间长开销大

自旋CAS如果长时间不成功,他会给CPU带来非常大的执行开销。因此CAS不适合竞争十分频繁的场景。

三、只能保证一个共性变量的原子操作

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

这里粘贴一个模拟CAS实现的计数器

/**
 * @Author: Soldier49Zed
 * @Date: 2019/9/26 0:31
 * @Description:
 */

class SimilatedCAS{
    private int value;
    public int getValue(){
        return value;
    }

    //这里只能用synchronized了  毕竟无法调用操作系统的CAS
    public synchronized boolean compareAndSwap(int expectedValue,int newValue){
        if (value == expectedValue){
            value = newValue;
            return true;
        }
        return false;
    }
}


public class CASCount implements Runnable{

    private SimilatedCAS counter = new SimilatedCAS();

    @Override
    public void run() {
        for (int i = 0;i < 10000;i++){
            System.out.println(this.increment());
        }
    }

    private int increment() {
        int oldValue = counter.getValue();
        int newValue = oldValue + 1;

        while (!counter.compareAndSwap(oldValue,newValue)) {//如果CAS失败,就去拿新值继续执行CAS
            oldValue = counter.getValue();
            newValue = oldValue + 1;
        }
        return newValue;
    }

    public static void main(String[] args) {
        Runnable run = new CASCount();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

}

猜你喜欢

转载自blog.csdn.net/Soldier49Zed/article/details/101356102