通过参数

时间:2018-04-29 15:59:44

标签: java android multithreading arraylist concurrency

假设我们有一个方法doSomething(String input),我们希望通过不同的输入同步运行它。

这意味着运行doSomething(A)应阻止doSomething(A)的所有连续调用,直到第一个调用完成为止,但不应阻止doSomething(B)doSomething(C)

所以我创建了一个包装器方法来实现这个目标。它根据输入值创建对象并对其进行锁定,并在列表中保留对它们的引用。

private static final ArrayList<String> runningTasks  = new ArrayList<>();


public void doSomethingSyncedByInput(String input) {

    // Create a lock or load an already created lock from the list.
    // (Yeah, it's a race condition but forget about it. It's just an example.)
    String lock = new String(input);
    if(runningTasks.contains(input)){
        // get currently available lock object
        lock = runningTasks.get(runningTasks.indexOf(input));
    }else {
        // add a reference on tasks list
        runningTasks.add(lock);
    }

    synchronized (lock) {
        doSomething(input);
    }
}

它确实有效;但它不是完全线程安全的解决方案,因为ArrayList不是线程安全的。 ArrayList的内容不易变,根据文档,添加和删除列表中的项目不会立即反映在其他线程上。

  

请注意,此实现未同步。如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。 / p>

ArrayList的一个众所周知的线程安全变体是CopyOnWriteArrayList(它会复制元素并重新设置内部volatile字段保存元素以确保具有最新版本立即在所有其他线程上的列表)。随着名称的产生,在向列表添加新项目时,每个列表项都 COPIES ,这意味着对已经锁定的实际对象的引用将丢失,代码将会丢失被打破了。

所以我需要一个包含对象列表的数据类型,并且不会复制或更改对象以支持并发支持。我可以使用String.intern()功能,但它会带来很多潜在的问题,特别是在移动平台上。

有什么想法吗?

或者,您知道可以使用的任何防弹实现吗?

顺便说一句,我在 Android 平台上。

更新:

我自己也找到了解决方案。看看我自己的答案。欢迎提出意见。

3 个答案:

答案 0 :(得分:0)

您不需要数据类型。您需要一个专用于锁定代码的信号量。在检查包含之前获取信号量。弄清楚你是否在列表中。如果需要,请将自己添加到列表中。之后发布。您现在可以通过同步方式获得任何锁定。

(这也基本上是任何同步类型都会做的 - 除了自己做信号量,你可以同时同步多个操作,比如检查包含然后添加到列表。它是使用同步数据类型的常见错误,期望它将解决您的问题,只是发现您需要在更高级别围绕多个功能进行同步)。

当然,您的代码还有其他弱点。你永远不会从列表中删除旧密钥,因此你可以在那里拥有无限大小。列表中还包含一个N操作,如果你更关心速度而不是内存,那么hashmap会更好。

答案 1 :(得分:0)

除非我误解了你的问题,否则这是一件微不足道的事情:

<T, R> R doSomethingSynchronizedByInput(T input, Function<T, R> fn) {
    synchronized(input) { return fn.apply(input); }
}

修改后注意到缺乏特异性并添加两个解决方案:

要做同样的事情,在输入的类/类型上,您只需稍微修改上面的代码:

<T, R> R doSomethingSynchronizedByInput(T input, Function<T, R> fn) {
    synchronized(input.getClass()) { return fn.apply(input); }
}

最后,对.equals的值做一些类似的事情(显然,你的意思是“值”)稍微复杂一些。这些方面应该有用:

// Note: I have not tested this: it is just a sketch.
// It requires that type T have an "equals" method that divides
// it into "values"
// Don't try to use a Comparator, because the HashMap doesn't.
public class DoSomethingSynchedByInput<T, R> {
    public interface Listener<V> { void accept(V val); }

    private final Map<T, LinkedList<T>> waiting = new HashMap<>();

    void doSomething(final T input, Function<T, R> fn, Listener<R> listener)
        throws InterruptedException {
        synchronized (waiting) {
            LinkedList<T> waitList = waiting.get(input);
            if (waitList == null) { waitList = new LinkedList<>(); }
            waitList.addLast(input);
            waiting.put(input, waitList);
            while (true) {
                if (waitList.peekFirst() == input) { break; }
                waiting.wait();
            }
        }
        try { listener.accept(fn.apply(input)); }
        finally {
            synchronized (waiting) {
                LinkedList<T> waitList = waiting.get(input);
                waitList.getFirst();
                if (waitList.size() > 0) { waiting.notifyAll(); }
                else { waiting.remove(input); } 
            }
        }
    }
}

请注意,此处有各种问题,您没有指定。有几个人提出了锁定公共对象可能是危险的问题(因为其他人可能)。这可能是个问题,也可能是设计目标。对我来说更重要的是,这个解决方案是阻塞线程而不是排队任务。被阻止的线程是表示排队工作的一种非常昂贵的方式。

我的建议是你采取上述解决方案并重新思考你的问题。但是,嘿,那不是你问的。

答案 2 :(得分:0)

注意:对于解决方案,请跳至下面的更新部分。

当我在CopyOnWriteArrayList课程中深入学习时,我注意到CopyOnWriteArrayList.add()使用了Arrays.copyOf(),这使得元素列表的浅拷贝。这意味着它只复制数组本身,而不是里面的元素。他们只是传递给新阵列。 因此,锁定对象保持不变,我们可以确定通过

检索到的对象
runningTasks.get(runningTasks.indexOf(input))

与我们在runningTasks上添加并锁定的对象完全相同,并且在编辑了列表项之后,列表立即显示在所有线程上的最新版本。

我在列表中进行了结构更改的实验,只是为了确定:

CopyOnWriteArrayList<String> l = new CopyOnWriteArrayList<>();
String s1 = new String("foo");
String s2 = new String("bar"); 
String s3 = new String("bar"); // Different object, same value

l.add(s1);
Log.e("TEST", "Result: "+String.valueOf( s1 == l.get(0) ));

l.add(s2);
Log.e("TEST", "Result: "+String.valueOf( s1 == l.get(0) ));
Log.e("TEST", "Result: "+String.valueOf( s2 == l.get(1) ));

l.add(s3);
Log.e("TEST", "Result: "+String.valueOf( s1 == l.get(0) ));
Log.e("TEST", "Result: "+String.valueOf( s2 == l.get(1) ));
Log.e("TEST", "Result: "+String.valueOf( s3 == l.get(2) ));

l.remove(1); // the s2
Log.e("TEST", "Result: "+String.valueOf( s1 == l.get(0) ));
Log.e("TEST", "Result: "+String.valueOf( s2 == l.get(1) )); // should be false
Log.e("TEST", "Result: "+String.valueOf( s3 == l.get(1) )); // should be true 

结果是:

E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: true
E/TEST: Result: false
E/TEST: Result: true

因此将保留对原始对象的引用。因此,可以修改代码以使用CopyOnWriteArrayList。最终版本将是:

private static final CopyOnWriteArrayList<String> runningTasks  = new CopyOnWriteArrayList<>();


public void doSomethingSyncedByInput(String input) {

    String lock;
    int index;

    synchronized (runningTasks) {
        index = runningTasks.indexOf(input);
        if (index >= 0) {
            // get currently available lock object
            lock = runningTasks.get(index);
        } else {
            // add a reference on tasks list
            lock = new String(input);
            runningTasks.add(lock);
        }
    }

    synchronized (lock) {
        if(!runningTasks.contains(lock)){
            runningTasks.add(lock);
        }

        doSomething(input);

        index = runningTasks.indexOf(lock);
        if(index >= 0)
            runningTasks.remove(index);
    }
}
但是,它并不完美。

欢迎提供反馈。

更新

我设法实现了更好的一个。这个是完全线程安全的,可以防止竞争条件并在使用后清理内存。

public class ParameterSynchronizer <T> {

    private final CopyOnWriteArrayList<T> objects;
    private final ConcurrentHashMap<T, Integer> lockCounter;

    public ParameterSynchronizer(){
        objects = new CopyOnWriteArrayList<>();
        lockCounter = new ConcurrentHashMap<>();
    }

    public T getLockObject(T input){
        synchronized (objects) {
            T lock = input;
            int index = objects.indexOf(lock);
            if (index >= 0) {
                lock = objects.get(index);
                lockCounter.put(lock, lockCounter.get(lock)+1);
            } else {
                objects.add(lock);
                lockCounter.put(lock, 1);
            }
            return lock;
        }
    }

    public void cleanUpLockObject(T input){
        synchronized (objects) {
            T lock = input;
            int counter = lockCounter.get(lock);
            if(counter == 1) {
                objects.remove(objects.indexOf(lock));
                lockCounter.remove(lock);
            }else{
                lockCounter.put(lock, counter - 1);
            }
        }
    }

}

<强>用法:

您应该创建一个具有此类实例的最终静态字段。使用getLockObject()获取synchronized阻止所需的对象。在同步块的末尾(最后一行,finallyreturn之前等),运行cleanUpLockObject()以清除内存。每个线程每次执行都应该调用两个方法,因为调用它们会改变线程计数器。

它跟踪锁定此对象的线程数,并在没有其他线程锁定时清除该对象。

private static ParameterSynchronizer<String> ps = new ParameterSynchronizer<>();

public void doSomethingSyncedByInput(String input){
    String lockObject = ps.getLockObject(input);
    synchronized (lockObject) {
        doSomething(input);
        ps.cleanUpLockObject(lockObject);
    }
}

以防万一,如果doSomething()抛出,可能会像

一样被捕获
private static ParameterSynchronizer<String> ps = new ParameterSynchronizer<>();

public void doSomethingSyncedByInput(String input) throws Exception {
    String lockObject = ps.getLockObject(input);
    synchronized (lockObject) {
        try {
            doSomething(input);
        } catch(Exception e) {
            throw e;
        } finally {
            ps.cleanUpLockObject(lockObject);
        }
    }
}