【问题标题】:Synchronize Java methods by the arguments通过参数同步 Java 方法
【发布时间】:2018-04-29 15:59:44
【问题描述】:

假设我们有一个方法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 实例,并且至少有一个线程在结构上修改了列表,则必须在外部同步。

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

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

有什么想法吗?

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

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

更新:

我自己已经找到了解决方案。看看我自己的答案。欢迎评论。

【问题讨论】:

  • String.valueOf(String) 什么都不做,它只会返回原始对象。
  • 现在你有一个竞争条件,你用indexOf检查runningTasks,然后如果没有找到则添加该值。
  • 看到你总是锁定某些东西,2 个线程上的 2 次调用试图获取 SAME 锁定意味着它们无论如何都会阻塞,因为你的代码有一个 synchronized 块,无论如何都使用它(从什么我知道您希望在锁定对象上进行初始同步,然后您不关心对该对象的后续调用)?此外,您已经知道 arraylist 访问不是 ttread 安全的
  • @markspace 是的,但这不是主要问题,为此实施安全的解决方法不会令人头疼。所以我专注于问题本身。

标签: java android multithreading arraylist concurrency


【解决方案1】:

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

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

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

【讨论】:

  • 我不明白信号量将如何提供帮助。它可能有助于保持列表上完成的事务的原子性,但ArrayList 的主要问题是列表的更改不会反映在其他线程上此对象的实例上,ArrayList.contains() 可能会返回 false而该项目已添加到另一个线程上。
  • @zxcmehran 您使用信号量来访问数组列表。这样,一次只有一个线程可以访问数组列表。由于所有线程都需要获取信号量,因此没有其他线程可以将任何内容添加到列表中。这就是 synchronized 关键字的作用——当你进入函数/块时它需要一个信号量,并在你退出时释放它。除了自己使用之外,您不限于每个信号量访问的单个函数调用,允许线程在调用之间进入。
  • @zxcmehran “对此列表的更改不会反映在其他线程上此对象的实例上” - 没有这样的事情。在您的代码中有一个数组列表。它由所有线程共享。它可以由多个线程编辑,但这些更改将立即出现,减去寄存器中先前结果的任何缓存。这不是您的代码的问题(当您反复检查变量以查找更改时,通常会出现问题,并且是通常通过使用 volatile 来修复,但这不是这些问题之一)。
  • “当您反复检查变量以寻找变化时,这通常是一个问题” - 该方法可能在任何条件下调用,因此应该认为它很重要。不,对ArrayList 的更改不会立即反映出来。我已经更新了问题。
  • 您根本不了解 Java 内存模型。在几台机器上运行一个编译器的输出一次,并不能证明任何事情。不要从我这里拿走。 Brian Goetz,Android Concurrency in Practice:“如果多个线程在没有适当同步的情况下访问同一个可变状态变量,那么您的程序就会损坏。”我不喜欢就 SO 进行辩论,但我已经厌倦了人们把这个简单的事情弄错了。
【解决方案2】:

除非我误解了你的问题,否则这是一件小事:

<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); } 
            }
        }
    }
}

请注意,这里有各种问题,您没有具体说明。有几个人提出了锁定公共对象可能很危险的问题(因为其他人可能会这样做)。这可能是个问题,也可能是一个设计目标。对我来说,更令人担忧的是,这个解决方案是阻塞线程而不是排队任务。阻塞线程是表示排队工作的一种非常昂贵的方式。

我的建议是您采用上述解决方案并重新考虑您的问题。但是,嘿,这不是你问的。

【讨论】:

  • 这不一定是正确的,因为同步锁是由对象完成的,而不是由值完成的。我已经提出了整个ArrayList 来解决这个问题。
  • 什么是“价值”。你的意思是类型/类?我想你可能误解了一些东西......
  • 哦...等等...如果两个对象是.equals,您是说要阻止它吗?如果仅此而已,并且 T == String,则只需实习字符串。
  • @G.BlakeMeike 锁定实习字符串是一种静态解决方案。其他任何东西都可以轻松锁定此字符串。
  • @G.BlakeMeike 此外,我在移动平台上,内存管理是一个严重的问题,即使它在一段时间后被 GC(很多 GC 使应用程序在弱 CPU 上成为滞后垃圾) )。
【解决方案3】:

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

当我深入了解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);
            }
        }
    }

}

用法:

您应该创建一个具有此类实例的 final static 字段。使用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);
        }
    }
}

【讨论】:

  • 如果doSomething 抛出会发生什么?
  • @G.BlakeMeike 可以使用try 块捕获它,对异常执行正确的操作,然后在finally 内运行cleanUpObject()
猜你喜欢
  • 2018-12-16
  • 1970-01-01
  • 2015-01-15
  • 2011-09-19
  • 1970-01-01
  • 1970-01-01
  • 2020-10-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多