使两个操作成为原子

时间:2014-10-03 15:55:52

标签: java multithreading

我正在实施应遵循以下要求的自定义数据结构:

  1. 元素应该是可搜索的(即根据.equals()定位和检索元素)
  2. 当我尝试检索元素但未找到时,该方法应该阻塞,直到这样的元素可用。如果永远不会,该方法将永久阻止或给定超时。
  3. 我已经实现了自己的类 - 我首先开始修改LinkedBlockingQueue - 但是我有一个问题,我可能会有一个错误的交错,我无法弄清楚如何避免一个。

    为了测试数据结构,我创建了一个包含3个线程的方法。这些线程中的每一个都将尝试接收唯一的字符串(例如," Thread1"用于第一个线程等...)。如果在数据存储区中找到该字符串,它将为下一个线程插入一个字符串(例如,线程1将插入"线程2")。

    这样,一个线程就会阻塞,只有当它的消息在商店中时才会发送消息。为了开始,在我开始我的线程之后,我手动添加" Thread1"到商店。这应该触发线程1从存储中获取其值并为线程2插入值。然后应该通知线程2并获取其值并为线程3等插入值。

    但是,我注意到在循环传递随机时间后循环停止。这是 - 我认为 - 由于takeOrWait()方法中可能出现的错误交错(如此处所示)。我尝试了几种解决方案,但我找不到办法。

    问题是 - 我认为 - 我必须释放锁以进行修改,然后调用sync.wait()。在这些调用之间,线程已经可以在队列中插入一个元素,这会导致所有等待的线程错过通知。

    在我解除锁定之前是否可以启动wait()

    for completness'我已经添加了我用来测试的Main.java类。

    BlockingDataStore

    public class BlockingStore<T> {
    
        // Linked list node.
        static class Node<E> {
            /**
             * The item, volatile to ensure barrier separating write and read
             */
            volatile E item;
            Node<E> next;
    
            Node(E x) {
                item = x;
            }
        }
    
    
        private Node<T> _head;
        private Node<T> _lastPtr;
        private int _size;
    
        // Locks
        private Lock changeLock = new ReentrantLock();
        private final Object sync = new Object();
    
        //////////////////////////////////
        //  CONSTRUCTOR                 //
        //////////////////////////////////
        public BlockingStore() {
            _head = null;
            _lastPtr = null;
        }
    
        //////////////////////////////////
        //  INTERNAL MODIFICATION       //
        //////////////////////////////////
    
        /**
         * Locates an element in the storage and removes it.
         * Returns null if the element is not found in the list.
         *
         * @param toRemove Element to remove.
         * @return Returns the removed element.
         */
        private T findAndRemove(T toRemove) {
            T result = null;
    
            // Empty queue.
            if (_head == null)
                return result;
    
            // Do we want the head?
            if (_head.item.equals(toRemove)) {
                result = _head.item;
                _head = _head.next;
                this._size--;
                return result;
            }
    
            Node<T> previous = _head;
            Node<T> current = previous.next;
    
            while (current != null) {
                if (current.item.equals(toRemove)) {
                    // Take the element out of the list.
                    result = current.item;
    
                    // we have to update the last pointer.
                    if (current == _lastPtr)
                        _lastPtr = previous.next;
                    else
                        previous.next = current.next;
                    this._size--;
                    return result;
                }
                previous = current;
                current = current.next;
            }
            return result;
        }
    
        /**
         * Adds an element to the end of the list.
         *
         * @param toAdd Element to add to the end of the list.
         */
        private void addToEnd(T toAdd) {
            // If the queue is empty
            if (_head == null) {
                _head = new Node<T>(toAdd);
                _lastPtr = _head;
            } else {
                _lastPtr.next = new Node<T>(toAdd);
                _lastPtr = _lastPtr.next;
            }
            this._size++;
        }
    
        /**
         * Takes an element from the front of the list.
         * Returns null if list is empty.
         *
         * @return Element taken from the front of the list.
         */
        private T takeFromFront() {
            // Check for empty queue.
            if (_head == null)
                return null;
    
            T result = _head.item;
            _head = _head.next;
            this._size--;
            return result;
        }
    
        //////////////////////////////////
        //  API METHODS                 //
        //////////////////////////////////
    
        /**
         * Takes an element from the datastore,
         * if it is not found the method blocks
         * and retries every time a new object
         * is inserted into the store.
         *
         * @param toTake
         * @return
         */
        public T takeOrWait(T toTake) {
            T value;
            changeLock.lock();
            value = findAndRemove(toTake);
    
            // Wait until somebody adds to the store
            // and then try again.
            while (value == null)
                // Sync on the notification object
                // such that we are waken up when there
                // is a new element.
                synchronized (sync) {
                    changeLock.unlock(); // allow writes.
                    // I think I can have bad inter-leavings here.
                    // If an insert is interleaved here, the thread
                    // will never be notified..
                    try {
                        sync.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    changeLock.lock();
                    value = findAndRemove(toTake);
                }
    
            changeLock.unlock();
            return value;
        }
    
        public T dequeue() {
            T value;
            changeLock.lock();
            value = takeFromFront();
            changeLock.unlock();
            return value;
        }
    
        public void enqueue(T toPut) {
            changeLock.lock();
            addToEnd(toPut);
    
            // Notify about new element in queue.
            synchronized (sync) {
                sync.notifyAll();
                changeLock.unlock();
            }
    
        }
    
        public int size() {
            return _size;
        }
    }
    

    ** Main.java **

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            final BlockingStore<String> q = new BlockingStore<String>();
    
            new Thread(new Runnable() {
                public String name = "Thread 1: ";
                public String to   = "Thread 2: ";
    
                public void print(String message) {
                    System.out.println(name + message);
                }
    
                @Override
                public void run() {
                    while (true) {
                        String value = q.takeOrWait(name);
                        print("Got: " + value);
                        q.enqueue(to);
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                public String name = "Thread 2: ";
                public String to   = "Thread 3: ";
    
                public void print(String message) {
                    System.out.println(name + message);
                }
    
                @Override
                public void run() {
                    while (true) {
                        String value = q.takeOrWait(name);
                        print("Got: " + value);
                        q.enqueue(to);
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                public String name = "Thread 3: ";
                public String to   = "Thread 1: ";
    
                public void print(String message) {
                    System.out.println(name + message);
                }
    
                @Override
                public void run() {
                    while (true) {
                        String value = q.takeOrWait(name);
                        print("Got: " + value);
                        q.enqueue(to);
                    }
                }
            }).start();
    
            Thread.sleep(1000);
            System.out.println("Main: Sending new message to queue for thread 1");
            q.enqueue("Thread 1: ");
    
        }
    
    }
    

1 个答案:

答案 0 :(得分:1)

  

问题是 - 我认为 - 我必须释放锁以进行修改,然后调用sync.wait()

听起来像丢失通知。如果sync.notify()中没有其他线程已被阻止,请理解sync.wait()什么都不做。 sync对象不记得已通知。

这不起作用(基于您的示例):

public void waitForFlag() {
    ...
    while (! flag) {
        synchronized (sync) {
            try {
              sync.wait();
            } catch (InterruptedException e) { ... }
        }
    }
}

public void setFlag() {
    flag = true;
    synchronized (sync) {
        sync.notifyAll();
    }
}

假设线程A调用waitForFlag(),发现标志为false,然后被抢占。然后,线程B调用setFlag(),通知no-one。最后,线程A调用sync.wait()。

现在你设置了标志,并且在wait()调用中阻塞了线程A,等待有人设置标志。这就是丢失通知的样子。

以下是它的外观:

public void waitForFlag() {
    ...
    synchronized(sync) {
        while (! flag) {
            try {
              sync.wait();
            } catch (InterruptedException e) { ... }
        }
    }
}


public void setFlag() {
    synchronized (sync) {
        flag = true;
        sync.notifyAll();
    }
}

这样,通知不会丢失,因为设置标志的语句和测试标志的语句都是同步块内。