【问题标题】:Synchronize by value rather that object按值而不是按对象同步
【发布时间】:2021-09-26 08:41:00
【问题描述】:

我实现了一个简单的锁定解决方案,它为一个值而不是对象创建一个锁,并且想了解专家对可能的性能或安全缺陷的看法。 这个想法是使用它来更新帐户余额,获取唯一帐号的锁定。

这是一个实现:

import java.util.*;

public class Mutex<T> {

    private final Set<T> set = new HashSet();

    public synchronized Lock acquireLock(
            T value
            ) throws InterruptedException {
        while(!set.add(value)) {
            this.wait();
        }
        return new Lock(value);
    }

    public class Lock {

        private final T value;

        public Lock(T value) {
            this.value = value;
        }

        public T getValue() {
            return value;
        }

        public void release() {
            synchronized(Mutex.this) {
                set.remove(value);
                Mutex.this.notifyAll();
            }
        }
    }
}

以下是检查可操作性的示例用法:

public class Test {

    private Mutex mutex = new Mutex();

    public static void main(String[] args) {
        Test test = new Test();
        Thread t1 = new Thread(() -> {
            try {
                test.test("SameValue");
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        });
        t1.setName("Thread 1");
        Thread t2 = new Thread(() -> {
            try {
                test.test("SameValue");
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        });
        t2.setName("Thread 2");

        t1.start();
        t2.start();
    }

    public void test(String value)
            throws
            InterruptedException {
        Lock lock = mutex.acquireLock(value);
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName());
        } finally {
            lock.release();
        }
    }
}

【问题讨论】:

  • 也许我遗漏了一些东西,但老实说,我不明白为什么需要这段代码。一个简单的synchronized (value) { ... } 不会达到完全相同的目的,消除对整个 Mutex 类的需要吗?当然,您可以稍微加强一下,以便所有等效字符串都引用同一个锁对象,但我认为您正在使用 Mutex 类重新发明轮子。
  • @CharlieArmstrong 同步块使用对象,我只有 Account IBAN 值(字符串)。我知道 String.intern() 方法,但它有 GC 问题。关于“所有等价的字符串都引用同一个锁对象”——我们有数百万个账户记录,所以将它们的 IBAN 存储在 HashMap 中并不是一个好的解决方案
  • 嗯,好的,我明白你现在在说什么。我不知道有什么好的解决方案。您的解决方案可能是最简单的方法,只是对我来说,这感觉就像必须有一种更内置的方法来做到这一点。如果没有一个 Java 库有这方面的功能,我会感到惊讶。
  • @CharlieArmstrong 同意,我也一样,这就是为什么我的问题在这里 :) 让我们看看
  • if(condition) … 中包装while(condition) … 循环是没有意义的,循环确实已经进行了预测试。当true 时,您只是执行了两次相同的测试。除此之外,既然您将list 更改为Set,您也不应该将变量命名为list,此外,add 合约已经足以测试存在性。换句话说,你可以简单地使用while(!set.add(value)) { this.wait(); } return new Lock(value); 就可以了。

标签: java concurrency locking


【解决方案1】:

关于您的实施,

我会使用 Set 而不是 List 来保存您的值(我假设这些值具有适当的等号/哈希码以使其有意义): List#contains 方法在 O(n) 中,如果您同时使用了很多 IBAN。

另外,您应该避免使用synchronize(this)(与方法上的同步关键字相同)。

为了解决你的问题,我使用了这样的东西:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Locks<T> {

    private final Lock lock = new ReentrantLock();

    //a Bimap from guava might be better here if you have the dependency
    //in your project
    private final Map<Reference<?>, T> valuePerReference = new HashMap<>();
    private final Map<T, Reference<Lock>> locks = new HashMap<>();

    private final ReferenceQueue<Lock> lockReferenceQueue = new ReferenceQueue<>();

    public Locks() {
        final Thread cleanerThread = new Thread(new Cleaner());
        cleanerThread.setDaemon(true);
        cleanerThread.start();
    }

    /**
     * @param value the value the synchronization must be made on
     * @return a lock that can be used to synchronize block of code.
     */
    public Lock getLock(T value) {
        lock.lock();
        try {
            return getExistingLock(value).orElseGet(() -> createNewLock(value));
        } finally {
            lock.unlock();
        }
    }


    private Optional<Lock> getExistingLock(T value) {
        return Optional.ofNullable(locks.get(value)).map(Reference::get);
    }

    private Lock createNewLock(T value) {
        //I create ReentrantLock here but a Supplier<Lock> could be a parameter of this
        //class to make it more generic. Same remark for SoftReference below.
        final Lock lock = new ReentrantLock();
        final Reference<Lock> reference = new SoftReference<>(lock, lockReferenceQueue);
        this.locks.put(value,reference);
        this.valuePerReference.put(reference,value);
        return lock;
    }


    private void removeLock(Reference<?> reference) {
        lock.lock();
        try {
            final T value = valuePerReference.remove(reference);
            locks.remove(value);
        } finally {
            lock.unlock();
        }
    }


    private class Cleaner implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    final Reference<? extends Lock> garbaged = lockReferenceQueue.remove();
                    removeLock(garbaged);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

}

然后我像这样使用它:

import java.util.concurrent.locks.Lock;

public class Usage {

    private final Locks<String> locks = new Locks<>();


    public void doSomethind(String iban) {
        final Lock lock = locks.getLock(iban);
        lock.lock();
        try {
            //.. do something with your iban
        } finally {
            lock.unlock();
        }
    }
}

虽然它使用 ReentrantLock,但代码可以很容易地修改为例如 ReadWriteLock。

【讨论】:

  • 感谢您的回答。但是关于 synchronized(this) 和 Thread 3:当线程 Thread 2 调用 wait 时,它会释放当前对象锁,因此即使 Thread 2 正在等待,也不应该阻塞线程 3
  • 你是对的。重新阅读您的代码后,似乎有一个错误,因为在while循环退出后没有重新添加该值。
  • 好发现!谢谢你:)
  • 为什么 OP 应该“避免使用 synchronize(this)”,但不能只使用不同的、未公开的对象?与 OP 的方法相比,哪个实际优势证明了这个明显更复杂的代码是合理的?
  • @Holger,最好使用内部对象进行同步(您可以在链接的帖子中获得更好的解释)。 OP实现很好。我的是另一个(未实现,因为不应使用 synchonize(this))。它有点复杂,因为它将锁的记忆和锁的使用分开。我还使用标准的 Java API 来锁定,因此可以使用它们的所有功能(如 await、signalAll)。使用 ReadWrtieLock 也很容易修改。
猜你喜欢
  • 2011-11-25
  • 1970-01-01
  • 2023-03-22
  • 2011-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-01
相关资源
最近更新 更多