java同步基于参数

Dou*_*eri 21 java multithreading synchronized

我正在寻找一种基于它接收的参数同步方法的方法,如下所示:

public synchronized void doSomething(name){
//some code
}
Run Code Online (Sandbox Code Playgroud)

我想doSomething基于这样的name参数同步方法:

线程1:doSomething("a");

线程2:doSomething("b");

线程3:doSomething("c");

线程4:doSomething("a");

线程1,线程2和线程3将执行代码而不同步,但线程4将等待,直到线程1完成代码,因为它具有相同的"a"值.

谢谢

UPDATE

根据都铎的解释,我认为我面临另一个问题:这是新代码的示例:

private HashMap locks=new HashMap();
public void doSomething(String name){
    locks.put(name,new Object());
    synchronized(locks.get(name)) {
        // ...
    }
    locks.remove(name);
}
Run Code Online (Sandbox Code Playgroud)

我没有填充锁定映射的原因是因为name可以有任何值.

基于上面的示例,由于HashMap不是线程安全的,因此在同一时间由多个线程添加/删除散列映射中的值时会出现问题.

所以我的问题是如果我创建了HashMap一个ConcurrentHashMap线程安全的,那么synchronized块会阻止其他线程访问locks.get(name)吗?

Tri*_*oan 17

特尔;博士:

我使用Spring Framework 中的ConcurrentReferenceHashMap。请检查下面的代码。


虽然这个话题很老了,但还是很有趣。因此,我想与 Spring Framework 分享我的方法。

我们试图实现的叫做命名互斥锁/锁。正如Tudor's answer所建议,这个想法是有一个Map来存储锁名称和锁对象。代码如下所示(我从他的回答中复制):

Map<String, Object> locks = new HashMap<String, Object>();
locks.put("a", new Object());
locks.put("b", new Object());
Run Code Online (Sandbox Code Playgroud)

但是,这种方法有两个缺点:

  1. OP已经指出了第一个:如何同步对locks哈希映射的访问?
  2. 如何去除一些不再需要的锁?否则,locks哈希映射将继续增长。

第一个问题可以通过使用ConcurrentHashMap来解决。对于第二个问题,我们有两个选择:手动检查并从映射中移除锁,或者以某种方式让垃圾收集器知道哪些锁不再使用,GC 将移除它们。我会选择第二种方式。

当我们使用HashMap, or 时ConcurrentHashMap,它会创建强引用。为了实现上面讨论的解决方案,应该使用弱引用来代替(要了解什么是强/弱引用,请参阅这篇文章这篇文章)。


所以,我使用Spring Framework 中的ConcurrentReferenceHashMap。如文档中所述:

一个ConcurrentHashMap使用的键和值软或弱引用。

此类可用作替代 Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>()),以便在并发访问时支持更好的性能。ConcurrentHashMap除了支持空值和空键之外,此实现遵循相同的设计约束 。

这是我的代码。该MutexFactory负责管理所有的锁用<K>是关键的类型。

@Component
public class MutexFactory<K> {

    private ConcurrentReferenceHashMap<K, Object> map;

    public MutexFactory() {
        this.map = new ConcurrentReferenceHashMap<>();
    }

    public Object getMutex(K key) {
        return this.map.compute(key, (k, v) -> v == null ? new Object() : v);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

@Autowired
private MutexFactory<String> mutexFactory;

public void doSomething(String name){
    synchronized(mutexFactory.getMutex(name)) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

单元测试(此测试使用awaitility库来处理某些方法,例如await(), atMost(), until()):

public class MutexFactoryTests {
    private final int THREAD_COUNT = 16;

    @Test
    public void singleKeyTest() {
        MutexFactory<String> mutexFactory = new MutexFactory<>();
        String id = UUID.randomUUID().toString();
        final int[] count = {0};

        IntStream.range(0, THREAD_COUNT)
                .parallel()
                .forEach(i -> {
                    synchronized (mutexFactory.getMutex(id)) {
                        count[0]++;
                    }
                });
        await().atMost(5, TimeUnit.SECONDS)
                .until(() -> count[0] == THREAD_COUNT);
        Assert.assertEquals(count[0], THREAD_COUNT);
    }
}
Run Code Online (Sandbox Code Playgroud)


Tud*_*dor 16

使用映射将字符串与锁定对象关联:

Map<String, Object> locks = new HashMap<String, Object>();
locks.put("a", new Object());
locks.put("b", new Object());
// etc.
Run Code Online (Sandbox Code Playgroud)

然后:

public void doSomething(String name){
    synchronized(locks.get(name)) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)


Tim*_*mos 10

都铎的答案很好,但它是静态的,不可扩展.我的解决方案是动态和可扩展的,但它在实现中增加了复杂性.外部世界可以像使用a一样使用Lock这个类,因为这个类实现了接口.您可以通过factory方法获取参数化锁的实例getCanonicalParameterLock.

package lock;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public final class ParameterLock implements Lock {

    /** Holds a WeakKeyLockPair for each parameter. The mapping may be deleted upon garbage collection
     * if the canonical key is not strongly referenced anymore (by the threads using the Lock). */
    private static final Map<Object, WeakKeyLockPair> locks = new WeakHashMap<>();

    private final Object key;
    private final Lock lock;

    private ParameterLock (Object key, Lock lock) {
        this.key = key;
        this.lock = lock;
    }

    private static final class WeakKeyLockPair {
        /** The weakly-referenced parameter. If it were strongly referenced, the entries of
         * the lock Map would never be garbage collected, causing a memory leak. */
        private final Reference<Object> param;
        /** The actual lock object on which threads will synchronize. */
        private final Lock lock;

        private WeakKeyLockPair (Object param, Lock lock) {
            this.param = new WeakReference<>(param);
            this.lock = lock;
        }
    }

    public static Lock getCanonicalParameterLock (Object param) {
        Object canonical = null;
        Lock lock = null;

        synchronized (locks) {
            WeakKeyLockPair pair = locks.get(param);            
            if (pair != null) {                
                canonical = pair.param.get(); // could return null!
            }
            if (canonical == null) { // no such entry or the reference was cleared in the meantime                
                canonical = param; // the first thread (the current thread) delivers the new canonical key
                pair = new WeakKeyLockPair(canonical, new ReentrantLock());
                locks.put(canonical, pair);
            }
        }

        // the canonical key is strongly referenced now...
        lock = locks.get(canonical).lock; // ...so this is guaranteed not to return null
        // ... but the key must be kept strongly referenced after this method returns,
        // so wrap it in the Lock implementation, which a thread of course needs
        // to be able to synchronize. This enforces a thread to have a strong reference
        // to the key, while it isn't aware of it (as this method declares to return a 
        // Lock rather than a ParameterLock).
        return new ParameterLock(canonical, lock);               
    }

    @Override
    public void lock() {
        lock.lock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        lock.lockInterruptibly();
    }

    @Override
    public boolean tryLock() {
        return lock.tryLock();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return lock.tryLock(time, unit);
    }

    @Override
    public void unlock() {
        lock.unlock();
    }

    @Override
    public Condition newCondition() {
        return lock.newCondition();
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,您需要一个给定参数的规范键,否则线程将不会被同步,因为它们将使用不同的Lock.规范化等同于Tudor解决方案中字符串的内化.哪里String.intern()本身是线程安全的,我的'规范池'不是,所以我需要在WeakHashMap上进行额外的同步.

此解决方案适用于任何类型的对象.但是,请确保在自定义类中实现equalshashCode正确实现,因为如果没有,则会出现线程问题,因为多个线程可能正在使用不同的Lock对象进行同步!

WeakHashMap的选择可以通过它带来的内存管理的简易性来解释.怎么可能知道没有线程正在使用特定的锁?如果可以知道,你怎么能安全地删除地图中的条目?您需要在删除时进行同步,因为您希望使用锁定的到达线程与从地图中删除锁定的操作之间存在竞争条件.所有这些都只是通过使用弱引用来解决,因此VM为您完成了工作,这大大简化了实现.如果您检查了WeakReference的API,您会发现依赖弱引用是线程安全的.

现在检查这个测试程序(由于某些字段的私有可见性,你需要从ParameterLock类内部运行它):

public static void main(String[] args) {
    Runnable run1 = new Runnable() {

        @Override
        public void run() {
            sync(new Integer(5));
            System.gc();
        }
    };
    Runnable run2 = new Runnable() {

        @Override
        public void run() {
            sync(new Integer(5));
            System.gc();
        }
    };
    Thread t1 = new Thread(run1);
    Thread t2 = new Thread(run2);

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

    try {
        t1.join();
        t2.join();
        while (locks.size() != 0) {
            System.gc();
            System.out.println(locks);
        }
        System.out.println("FINISHED!");
    } catch (InterruptedException ex) {
        // those threads won't be interrupted
    }
}

private static void sync (Object param) {
    Lock lock = ParameterLock.getCanonicalParameterLock(param);
    lock.lock();
    try {
        System.out.println("Thread="+Thread.currentThread().getName()+", lock=" + ((ParameterLock) lock).lock);
        // do some work while having the lock
    } finally {
        lock.unlock();
    }        
}
Run Code Online (Sandbox Code Playgroud)

很可能你会发现两个线程都使用相同的锁对象,因此它们是同步的.示例输出:

Thread=Thread-0, lock=java.util.concurrent.locks.ReentrantLock@8965fb[Locked by thread Thread-0]
Thread=Thread-1, lock=java.util.concurrent.locks.ReentrantLock@8965fb[Locked by thread Thread-1]
FINISHED!
Run Code Online (Sandbox Code Playgroud)

但是,有可能两个线程在执行时不会重叠,因此不要求它们使用相同的锁.您可以通过在正确的位置设置断点,在调试模式下轻松强制执行此行为,从而强制第一个或第二个线程在必要时停止.您还会注意到,在主线程上的垃圾收集之后,WeakHashMap将被清除,这当然是正确的,因为主线程Thread.join()在调用垃圾收集器之前等待两个工作线程通过调用完成其工作.这确实意味着在工作线程内不再存在对(参数)锁的强引用,因此可以从弱hashmap中清除引用.如果另一个线程现在想要在同一参数上同步,则将在同步部分中创建一个新的锁getCanonicalParameterLock.

现在用任何具有相同规范表示的对重复测试(=它们是相等的,所以a.equals(b)),并看到它仍然有效:

sync("a");
sync(new String("a"))

sync(new Boolean(true));
sync(new Boolean(true));
Run Code Online (Sandbox Code Playgroud)

等等

基本上,这个类为您提供以下功能:

  • 参数化同步
  • 封装内存管理
  • 用任何类型的对象的工作能力(的条件下,equalshashCode正确地实现的)
  • 实现Lock接口

这个Lock实现已经过测试,同时修改了一个ArrayList,迭代了10次迭代1000次,执行此操作:添加2个项目,然后通过迭代完整列表删除最后找到的列表条目.每次迭代都会请求锁定,因此总共需要10*1000个锁.没有引发ConcurrentModificationException,并且在所有工作线程完成后,项目总数为10*1000.在每次修改时,都通过调用请求锁定ParameterLock.getCanonicalParameterLock(new String("a")),因此使用新的参数对象来测试规范化的正确性.

请注意,您不应该为参数使用字符串文字和基本类型.由于字符串文字是自动实现的,因此它们总是具有强引用,因此如果第一个线程以其参数的字符串文字到达,则锁定池将永远不会从条目中释放,这是内存泄漏.自动装箱原语也是如此:例如,Integer有一个缓存机制,它将在自动装箱过程中重用现有的Integer对象,同时也会导致存在强引用.然而,解决这个问题,这是一个不同的故事.