michael&scott算法

目录

 

1 介绍

2 算法

3 正确性

4 进一步实现

你的打赏是我奋笔疾书的动力!​


1 介绍

         回顾以往研究者的各种blocking算法、non-blocking算法、lock-free算法,它们要么基于并发的FIFO队列,要么基于单向循环链表,要么基于compare_and_swap原语,甚至基于double_compare_and_swap原语。然而,简单、快速、实用的非阻塞和阻塞并发队列算法的发现,令michaelscott很惊讶,因为在以往的文献中从未有过。

         算法中出现的ABA问题描述:如果一个进程在一个内存共享的位置上读到了一个值A,并计算了一个新值,打算使用compare_and_swap原子操作设置这个共享位置上的值为新值,如果在读到了值A和打算设置之间,另外一个进程改变了值AB,并且再次又改回了值A,然而第一个进程的原子操作可能是成功的。

2 算法

         1显示了非阻塞的注释伪代码 队列数据结构和操作。该算法将队列实现为具有HeadTail指针的单链表。与Valois的算法一样,Head始终指向哑节点,这是列表中的第一个节点。Tail指向列表中的最后一个节点或倒数第二个节点。该算法使用compare_and_swap,使用修改计数器来避免ABA问题。为了允许队列出列以释放出列节点,出队操作确保Tail不指向出列节点或任何前任节点。这意味着可以安全地重新使用出列节点。

         为了获得我们所依赖的各种指针指向的一致值,重新检查早期值的读取的序列以确保他们没有改变是肯定的。这些读取序列和普拉卡什等人的快照是相似的,而不是比之更简单。 (我们只需要检查一个共享变量而不是两个)。一个在Stone的阻塞算法中的类似的技术可用于防止竞争条件。我们使用Treiber的简单和高效的非阻塞堆栈算法[21]来实现一个非阻塞列表。

         2显示了twolock的注释伪码的队列数据结构和操作。算法采用单独的头部锁和尾部锁,以完成 enqueuesdequeues之间的并发性。就像在非阻塞队列中,我们在列表的开头保留一个哑节点。由于哑节点,入队永远不必访问Head,而且dequeuers永远不必访Tail,从而避免因各个进程尝试以不同的顺序获取锁而产生的潜在死锁问题。

 

3 正确性

         算法是安全的,因为它们满足以下属性:

         1.链表始终是连接的。

         2.链表中新增的节点仅在最后一个节点之后插入。

         3.节点仅从链表的开头删除。

         4. Head总是指向链表中的第一个节点。

         5. Tail总是指向链表中的节点。 (例如:不会指向被删除的节点)

         最初,所有这些属性都成立。通过归纳,我们证明他们将继续持有这些属性,假设ABA 问题永远不会发生

  1. 链接列表始终连接的,因为一旦新节点被插入到对列中,则该节点在释放之前其next指针不会被设置为NULL,且直到从队列头部删除出队列该节点才会被释放(属性3)。
  2. 在无锁算法中,新增节点仅会在链表的末尾被插入,因为它要通过Tail指针链接,并且Tail指针总是指向链表中的一个节点(属性5),新插入的节点被链接到一个具有NULL值的next指针的节点, 并且只有这样一个节点是链表中的最后一个节点 (属性1)。 在基于锁的算法中,新增节点仅会在链表的末尾被插入,那是因为新增节点被插入到Tail指向的节点的后面,在此算法Tail总是指向链表中的最后一个节点,Tailtail lock保护。
  3. 节点从列表的开头删除,因为节点只有被Head指针指向时才会被删除,并且Head总是指向列表中的第一个节点(属性4)。
  4. Head总是指向列表中的第一个节点,因为它只是原子地将其更改为下一个节点 (使用头锁或使用compare_and_swap)。当这发生时,它Head指向的节点被视为从列表中删除。
  5. Tail总是指向链表中的节点,因为它永远不会落后于Head,所以它永远不会指向一个已被删除节点。此外,当Tail更改其值时,总是摆动到列表中Tail的下一个节点,如果Tailnext指针为NULL,它永远不会尝试更改其值。
	structure pointer_t {
		ptr: pointer to node_t, 
		count: unsigned integer
	}
	
	structure node_t {
		value: data type,
		next: pointer_t
	}
	
	structure queue_t {
		Head: pointer_t, 
		Tail: pointer_t
	}

 

4 进一步实现

         java语言来实现这个算法(只实现了入队方法),可能的实现如下:

package net.jcip.examples;

import java.util.concurrent.atomic.*;

import net.jcip.annotations.*;

/**
 * LinkedQueue
 * <p/>
 * Insertion in the Michael-Scott nonblocking queue algorithm
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class LinkedQueue <E> {

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

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

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

    public boolean put(E item) {
        LinkedQueue.Node<E> newNode = new LinkedQueue.Node<E>(item, null);
        while (true) {
            LinkedQueue.Node<E> curTail = tail.get();
            LinkedQueue.Node<E> tailNext = curTail.next.get();
            if (curTail == tail.get()) {
                if (tailNext != null) {	A
                    //队列处于中间状态,推进tail指针的指向(advance it)
                    tail.compareAndSet(curTail, tailNext);	B
                } else {
                    // 在稳定状态下,尝试插入新节点
                    if (curTail.next.compareAndSet(null, newNode)) {	C
                        // 成功插入新节点,推进tail指针的指向(advance it)
                        tail.compareAndSet(curTail, newNode);	D
                        return true;
                    }
                }
            }
        }
    }
}

         插入新的元素涉及到两个指针的更新。首先通过更新当前队尾元素的next指针把新节点链接到队尾元素;然后释放tail指针,让其重新指向新的队尾元素。

         所以在这两个操作之间队列可能处于中间状态(当前获取的队尾元素的next指针不为null),如下图:

         在第二次更新后,队列再次处于稳定状态(当前获取的队尾元素的next指针为null),如下图:

 

         put方法在插入新元素之前,将首先检查队列是否处于中间状态(步骤A),如果是,那么有另一个线程正在插入元素(在步骤CD之间)。此时线程不会等待其他线程执行完成,而是帮助它完成操作,并将尾节点向前推进一个节点(步骤B)。然后,它将重复执行这种检查,以免另一个线程已经开始插入新元素,并继续推进尾节点,直到它发现队列处于稳定状态后,才会开始执行自己的插入操作。

         由于步骤C中的CAS将把新节点链接到队列尾部,因此如果两个线程同时插入元素,那么这个CAS将失败。在这样的情况下,并不会造成破坏:不会发生任何变化,并且当前的线程只需重新读取尾节点并再次重试。如果步骤C成功了,那么插入操作将生效,第二个CAS(步骤D)被认为是一个“清理操作”因为它既可以由执行插入操作的线程来执行,也可以由其他任何线程来执行。如果步骤D失败,那么执行插入操作的线程将返回,而不是重新执行CAS,因为不再需要重试——另一个线程已经在步骤B中完成了这个工作。这种方式能够工作,因为在任何线程尝试将一个新节点插入到队列之前,都会首先通过检查tail.next是否非空来判断是否需要清理队列。如果是,它首先会推进为尾节点(可能需要执行多次),直到队列处于稳定状态。

         需要说明的是这个实现没有处理ABA的问题,我们可以把AtomicReference换成AtomicStampedReference来应对这个问题。

5 ConcurrentLinkedQueue

略。

你的打赏是我奋笔疾书的动力!

支付宝打赏:

微信打赏:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 数据结构算法对于计算机科学领域的学习和实践非常重要。作为一门程序设计语言,Java提供了丰富的数据结构和算法库,使得开发人员能够有效地处理和操作不同类型的数据。 在Java中,常用的数据结构包括数组、链表、栈、队列、堆、树和图等。这些数据结构能够满足各种不同的需求,例如数组适用于随机访问,链表适用于插入和删除操作,栈和队列适用于特定的数据处理流程。 同时,Java也提供了丰富的算法来处理这些数据结构。常见的算法包括排序算法(如冒泡排序、快速排序、归并排序)、搜索算法(如线性搜索、二分搜索)、图算法(如广度优先搜索、深度优先搜索)等。这些算法能够帮助开发人员解决实际的问题,提高程序的效率和性能。 Michael T.是一位非常有经验和技术水平的专家,他在Java数据结构和算法方面有着深入的研究和理解。他能够利用Java提供的数据结构和算法库来解决复杂的问题,并根据需求选择恰当的算法和数据结构来达到最佳效果。 除了掌握Java提供的数据结构和算法库,Michael T.还在实践中不断学习和探索新的数据结构和算法。他会进一步学习和了解高级的数据结构和算法,如红黑树、图算法和动态规划等,以更好地应对各种复杂的问题和挑战。 总而言之,Java 数据结构算法是一门重要的学科,它不仅能够提供常用的数据结构和算法库,还能够让开发人员通过深入研究和实践来提高自己的编程能力。Michael T.作为一位专家,他对Java数据结构算法的研究和应用能力使得他在这个领域非常有价值和竞争力。他不仅能够解决实际问题,还能够不断进一步探索和创新,为整个行业带来更多的惊喜和突破。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值