如何使我的数据结构线程安全?

时间:2010-07-03 02:25:35

标签: java class thread-safety

我定义了一个Element类:

class Element<T> {
    T value;
    Element<T> next;

    Element(T value) {
        this.value = value;
    }
}

还定义了一个基于Element的List类。它是一个典型的列表,就像在任何数据结构书籍中一样,具有addHead,delete等操作

public class List<T> implements Iterable<T> {
    private Element<T> head;
    private Element<T> tail;
    private long size;

    public List() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    public void insertHead (T node) {
        Element<T> e = new Element<T>(node);
        if (size == 0) {
            head = e;
            tail = e;
        } else {
            e.next = head;
            head = e;
        }
        size++;     
    }

    //Other method code omitted
}

如何使这个List类线程安全?

对所有方法进行同步?似乎不起作用。两个线程可能同时在不同的方法上工作并导致冲突。

如果我使用数组来保留类中的所有元素,那么我可以在数组上使用volatile来确保只有一个线程正在使用内部元素。但是目前所有元素都通过每个下一个指针上的对象引用链接。我无法使用volatile。

在头部,尾部和大小上使用挥发性物质?如果两个线程运行不同的方法保持资源彼此等待,这可能会导致死锁。

有什么建议吗?

3 个答案:

答案 0 :(得分:3)

如果在每个方法上放置synchronized,数据结构将是线程安全的。因为根据定义,一次只有一个线程将在对象上执行任何方法,并且还确保了线程间的排序和可见性。所以它就像一个线程正在进行所有操作一样好。

如果块覆盖的区域是整个方法,则放置synchronized(this)块将没有任何不同。如果面积小于此值,您可能会获得更好的性能。

做类似

的事情
private final Object LOCK = new Object();

public void method(){
    synchronized(LOCK){
        doStuff();
    } 
}

被认为是良好的做法,虽然不是为了更好的表现。这样做可以确保没有其他人可以使用您的锁,并且无意中创建了一个容易出现死锁的实现等。

在您的情况下,我认为您可以使用ReadWriteLock来获得更好的读取性能。顾名思义,ReadWriteLock允许多个线程通过,如果他们正在访问“读取方法”,这些方法不会改变对象的状态(当然,你必须正确识别你的哪个方法是“读取方法”) “和”写方法“,并相应地使用ReadWriteLock!)。此外,它确保在执行“写入方法”时没有其他线程正在访问该对象。它负责读/写线程的调度。

使类线程安全的其他众所周知的方法是“CopyOnWrite”,您可以在突变时复制整个数据结构。仅当对象主要是“读取”并且很少“写入”时才建议这样做。

以下是该策略的示例实现。 http://www.codase.com/search/smart?join=class+java.util.concurrent.CopyOnWriteArrayList

private volatile transient E[] array;

/**
 * Returns the element at the specified position in this list.
 *
 * @param  index index of element to return.
 * @return the element at the specified position in this list.
 * @throws    IndexOutOfBoundsException if index is out of range <tt>(index
 *            < 0 || index >= size())</tt>.
 */
public E get(int index) {
    E[] elementData = array();
    rangeCheck(index, elementData.length);
    return elementData[index];
}
/**
 * Appends the specified element to the end of this list.
 *
 * @param element element to be appended to this list.
 * @return true (as per the general contract of Collection.add).
 */
public synchronized boolean add(E element) {
    int len = array.length;
    E[] newArray = (E[]) new Object[len+1];
    System.arraycopy(array, 0, newArray, 0, len);
    newArray[len] = element;
    array = newArray;
    return true;
}

这里,read方法是在不经过任何锁定的情况下访问,而write方法必须是synchronized。通过对数组使用volatile来确保读取方法的线程间排序和可见性。

写入方法必须“复制”的原因是因为赋值array = newArray必须是“一次性”(在java中,对象引用的赋值是原子的),并且您可能不会触及原始数组操纵。

答案 1 :(得分:1)

我会查看java.util.LinkedList类的源代码以获得真正的实现。

默认情况下,同步会锁定类的实例 - 这可能不是您想要的。 (特别是如果Element可从外部访问)。如果你在同一个锁上同步所有方法,那么你将有可怕的并发性能,但它会阻止它们同时执行 - 实际上是对类的单线程访问。

另外 - 我看到一个尾部参考,但是没有看到带有相应前一个字段的Element,对于双链表 - 原因?

答案 2 :(得分:1)

我建议您使用可以传递给列表中每个元素的ReentrantLock,但是您必须使用工厂来实例化每个元素。

任何时候你需要从列表中取出一些东西,你将阻止同一个锁,这样你就可以确保没有两个线程同时访问。