JAVA并发编程实战-原子变量与非阻塞同步机制

思维导图

在这里插入图片描述

1 锁的劣势

使用一致的加锁协议保障对共享状态的安全访问。

当锁的竞争频率非常高,调度和真正用于工作的开销将会非常客观。

加锁是线程安全的,但是也有缺点:

  • 加锁导致其它线程不能做任何事情或者被挂起,性能会由于上下文切换和调度受到损失。
  • 性能风险-优先级倒置,一个低优先级线程加锁成功,但是持有锁期间发生了错误:调度情况和页错误,将导致其它高优先级线程被阻塞。

2 硬件对并发的支持

独占锁是排它锁,悲观锁。对于细粒度的操作,有更加有效的乐观锁,通过冲突检测判断是否被其它线程修改。

多处理器也提供了特殊的原子指令,来管理并发的访问的共享数据。如获取并增加、交换指令和比较并交换等。

  • 这些原子指令的实现多是依赖于硬件的总线加锁或者是缓存锁,来保证对多处理器的协调。

2.1 比较并交换

比较并交换CAS有三个操作数——内存位置V、旧的预期值A和新的更改值B。当A和真实值一致才会更改,否则什么都不做。
下面的demo-1解释了CAS的语义(非实现)。

/**
 * 模拟cas
 */
public class SimulatedCas {
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized int compareAndSwap(int expectValue, int newValue) {
        int oldValue = getValue();
        if (expectValue == oldValue) {
            value = newValue;
        }
        return oldValue;
    }

    public synchronized boolean compareAndSet(int expectValue, int newValue) {
        return expectValue == compareAndSwap(expectValue, newValue);
    }
}

通常CAS操作都是在循环中进行,当失败会进行重试,不会将线程进行挂起,减少了锁与相关的活跃度风险。

2.2 非阻塞计数器

下列的demo-2利用CAS模拟了线程安全的计数器:

//基于CAS实现的非阻塞计数器
public class CasCounter {
    private  SimulatedCas simulatedCas;

    public int getValue() {
        return simulatedCas.getValue();
    }

    public int increment() {
        int v;
        do {
            v = getValue();
        } while (v != simulatedCas.compareAndSwap(v, v+1));
        return v+1;
    }
}

当成功直接返回,否则进行重试,直到成功。

通常来说基于CAS的计数器性能要远远由于基于锁的计数器。

java的package java.util.concurrent.atomic已经提供了各种原子类,供我们使用。

3 原子变量类

原子变量类比锁更轻量,因为它不会引起线程的挂起和重新调度。

3.1 原子变量是更佳的volatile

比如我们可以使用原子化来减少竞争条件,比如下列的demo-3:

public class CasNumberRange {

    private final AtomicReference<IntPair> values = new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }
    public int getUpper() {
        return values.get().upper;
    }

    public void setLower(int newValue) {
        while (true) {
            int upper = getUpper();
            IntPair old = values.get();
            if (upper<newValue) {
                throw new IllegalStateException();
            }
            IntPair intPair = new IntPair(newValue, upper);
            if (values.compareAndSet(old, intPair)) {
                return;
            }
        }

    }

    @Data
    private static class IntPair {
        final int lower;
        final int upper;

    }
}

通过原子化的compareAndSet底层调用比较并交换来实现更新边界条件。

4 非阻塞算法

一个线程的失败和挂起不应该影响其它线程的失败和挂起,这样的算法被称为非阻塞。

在线程间使用CAS进行协调,可以构建非阻塞的算法。

4.1 非阻塞栈

创建非阻塞算法的前提是维持数据的一致性。

下列demo-4演示了如何使用原子引用来构建栈:

/**
 * 使用cas的非阻塞栈
 * @param <E>
 */
public class ConcurrentStack<E> {
    private final AtomicReference<Node<E>> top = new AtomicReference<>();

    public void push(E item) {
        Node<E> oldNode;
        Node<E> newNode = new Node<>(item);
        do {
            oldNode = top.get();
            newNode.next = oldNode;
        }while (!top.compareAndSet(oldNode, newNode));
    }

    public E pop() {
        Node<E> oldNode;
        Node<E> newHead;
        do {
            oldNode = top.get();
            if (oldNode == null) {
                return null;
            }
            newHead = oldNode.next;
        } while (!top.compareAndSet(oldNode, newHead));
        return oldNode.item;
    }

    private static class Node<E> {
        private final E item;
        private Node next;

        public Node(E item) {
            this.item = item;
        }
    }
}

这里我们把原子化范围缩到唯一变量栈顶top,出栈和入栈就是对栈顶top的原子化更新。从而实现非阻塞的栈。

这里ConcurrentStack之所以线程安全,主要来源于:compareAndSet提供的原子性和可见性保证。

4.2 非阻塞链表

下面的demo-5演示了一个非阻塞链表的插入过程:

public class LinkedQueue<E> {
    private static class Node<E> {
        final E item;
        final AtomicReference<Node<E>> next;

        public Node(E item, AtomicReference<Node<E>> next) {
            this.item = item;
            this.next = next;
        }
    }

    private final Node<E> dummy = new Node<>(null, null);
    private final AtomicReference<Node<E>> head = new AtomicReference<>(dummy);
    private final AtomicReference<Node<E>> tail = new AtomicReference<>(dummy);

    /**
     * 非阻塞队列
     * @param item
     * @return
     */
    public boolean put(E item) {
        Node<E> eNode = new Node<>(item, null);
        while (true) {
            Node<E> curTail = tail.get();
            Node<E> tailNext = curTail.next.get();
            //如果不相等,说明被其它线程修改,再次循环
            if (curTail == tail.get()) {
                //如果不为null,说明上一个线程插入新节点,但是cas修改tail失败,需要帮助修改tail
                if (tailNext != null) {
                    tail.compareAndSet(curTail, tailNext);
                } else {
                    //可以插入新节点
                    if (curTail.next.compareAndSet(null, eNode)) {
                        //尝试修改tail,失败成功都会返回,由其它线程put时会进行判断
                        tail.compareAndSet(curTail, eNode);
                        return true;
                    }
                }
            }
        }
    }
}

这里主要是由几个步骤保证了线程安全:

  • 首先获取tail节点,判断是否修改,如果被修改进行重试,否则进入下一步。
  • 判断tail的next是否为null,正常为null,不为null说明其它线程正在进行修改或者修改失败了,此时需要协助cas更改tail到next的节点。
  • 否则可以进行正常的新节点插入,通过cas更改tail的next,如果成功进行下一步,否则重试。
  • 改变tail指针到新节点,这里不管cas成功失败,都会返回true。因为我们在循环开始进行了判断,进行处理tail的指向问题。

4.3 ABA问题

ABA问题是因为算法误用比较并交换而引起的反常现象。

可以在更新引用变量时,加上一个版本号的更新。

参考文献

[1].《JAVA并发编程实战》.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LamaxiyaFc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值