多线程环境中的对象深度锁定

时间:2012-10-22 13:33:48

标签: java multithreading concurrency

我的项目中有以下类结构 A类 B类

class A {
    private String id;
    private List<B> bs = new ArrayList<B>();

    A(String id){
        this.id = id;
    }

    public List<B> getBs(){
        return bs;
    }

    public void setBs(List<B> bs){
        this.bs=bs;
    }

    public void addBs(List<B> bs){
        this.bs.addAll(bs);
    }

    public void addB(B b){
        this.bs.add(b);
    }
}

class B {
    private String id;
    private List<String> tags;

    B(String id){
        this.id = id;
    }

    public List<String> getTags(){
        return tags;
    }

    public void setTags(List<String> tags){
        this.tags = tags;
    }

    public void addTags(List<String> tags){
        this.tags.addAll(tags);
    }

    public void addTag(String tag){
        this.tags.add(tag);
    }
}

我们还有一个缓存类:

class CacheService {
    private static final ConcurrentHashMap<String, Object> CACHE = new ConcurrentHashMap<String, Object>();

    public static Object get(String id){
        return CACHE.get(id);
    }

    public static void put(String id, Object obj){
        return CACHE.put(id, obj);
    }
}

现在,使用唯一ID创建A类和B类的对象,并通过&lt; id,object&gt;放入此缓存中。组合。例如:

A a1 = new A("10");
CacheService.put("10", a1);

A a2 = new A("11");
CacheService.put("11", a2);

B b1 = new B("1");
CacheService.put("1", b1);

B b2 = new B("2");
CacheService.put("2", b2);

B b3 = new B("3");
CacheService.put("3", b3);

B b4 = new B("4");
CacheService.put("4", b4);

此外,我将B类对象放在List<B>内部对象a1a2中。 重要的是要注意,唯一的B对象只在任何A对象中放置一次

a1.add(b1);
a1.add(b2);

a2.add(b3);
a2.add(b4);

这样我们就可以在CACHE中为A和B类提供多个对象。

场景:现在多个线程访问此CACHE,但其中一些线程最终获取A类对象,而其他B类对象则取决于用户指定的ID。这些主题实际上想要对这些对象进行读取更新信息。

问题:我的要求是当一个线程访问了类A的对象(例如a1)来更新它时,其他任何线程都不能读取< / strong>或更新 a1以及添加到b1的所有B类对象(本例中为b2List<B>)内部对象a1,直到我完成a1上的所有更新。请告诉我如何在这种情况下获得锁定?

3 个答案:

答案 0 :(得分:3)

要进行深度复制,我建议您在A和B的每个实例中都有一个Lock,并让它们都使用lock()unlock()方法实现一些接口,其中class {{ 1}}将获得自己的锁,以及A的所有锁。然后,在使用之前锁定一个对象,并在完成后解锁。

编辑:所以你的行动方针是:

  1. 创建一个界面,让我们用两种方法称它B'sLockablelock()
  2. 让A和B都实现该接口。因此,您的缓存现在将使用unlock()代替Lockable
  3. 为A和B添加私有字段

    私人决赛Lock lock = new ReentrantLock();

  4. 现在在B中实现Object只是为了在锁
  5. 上调用相同的方法
  6. 在A中,Lockable将获取本地锁定实例,并迭代b列表并调用它们的lock()方法。 lock()
  7. 也是如此
  8. 现在,每次从缓存中获取对象时,在对其执行任何操作之前,请在该对象上调用unlock(),然后在完成后调用lock()

答案 1 :(得分:2)

synchronized关键字可以帮助您解决此问题。您可以将整个方法声明为synchronized,也可以使用synchronized()块,这些块会锁定您定义的特定键对象。输入synchronized()块时,另一个线程无法访问锁定该相同键对象的其他块,直到退出该块为止。

请参阅同步here的Java教程。

对于您的示例,您可以执行以下任一操作:

public synchronized void addB(B b) {
    this.bs.add(b);
}

OR

...在你的班级中声明一个锁定对象......

private final Object LOCK = new Object();

...并使用synchronized()块:

public void addB(B b) {
    synchronized(LOCK) {
        this.bs.add(b);
    }
}

使用第二个优先于第一个的优点是您可以完全,明确地控制哪些代码段被锁定(而不仅仅是整个方法调用)。在处理并发时,为了提高效率,您希望尽可能少地进行同步,因此使用此方法可以仅在最低限度上执行同步。

此外,我确信有人会指出您不需要显式声明另一个锁对象,因为您可以使用this关键字在当前对象上进行同步。但是,this other StackOverflow question and answer总结了我不建议这么做的原因。

答案 2 :(得分:1)

EDIT2:在问题被编辑后我打算完成我的答案,但我刚刚找到了课程java.util.concurrent.locks.ReentrantReadWriteLock,其中我认为完全符合需求用户sohanlal。

这个类提供了一对锁,一个用于读取操作,可以由多个元素同时拥有,并且本身不是阻塞的。第二个(写入部分)在获取时避免任何读取或写入操作。

解决方案如下:

  1. 向容器类A添加锁。此类是读写锁的真正所有者。
  2. 将锁属性添加到类B.此类不拥有锁,但只引用它来读取锁。
  3. 将新B对象添加到A容器时,请为B对象提供对容器上ReentrantReadWriteLock的引用。从A中提取B对象时,取消引用从容器中“继承”的锁。
  4. 在A类中,对每个修改B对象集合的操作进行写锁定(add,removew ...)。 (可选)(如果B元素的基础集合未同步),则不会修改集合但访问它的读锁定操作。
  5. 在B类中,对每个操作进行读锁定。
  6. 代码。这尚未经过测试,但会了解应如何实施:

    class A {
        private String id;
        private List<B> bs = new ArrayList<B>();
        private ReentrantReadWriteLock lk = new ReentrantReadWriteLock();
    
        A(String id){
            this.id = id;
        }
    
        public List<B> getBs(){
            lk.readLock().lock(); // acquire the read lock, since this operation does not affect A contents
            return bs;
            lk.readLock().unlock();
        }
    
        public void setBs(List<B> bs){
            lk.writeLock().lock(); // acquire the write lock, which automatically avoids further reading/writing operations
            this.bs=bs;
            for( B elem : bs )
            {
                // internal B elements need a reference to the reading part of the lock
                elem.setLock(lk.readLock()); 
            }
            lk.writeLock().unlock();
        }
    
        public void addBs(List<B> bs){
            [...] // similar approach that in setBs
        }
    
        public void addB(B b){
            [...] // similar approach that in setBs
        }
    
        public void deleteB( B elem ) // Or whatever notation you want
        {            
            lk.writeLock().lock(); // acquire the write lock
            B internalElem = bs.get(bs.indexOf(elem));             
            if( internalElem != null )
            {
                bs.remove(internalElem);
                bs.unsetLock();
            }
            lk.writeLock().unlock();
        }
    }
    
    class B {
        private String id;
        private List<String> tags;
        private Lock lk;
    
        B(String id){
            this.id = id;
            lk = null;
        }
    
        public void setLock(Lock l){ lk = l; } // put additional controls if you want
        public void unsetLock()
        { 
            lk = null; 
        }
    
        private void lockInternal()
        {
            if(lk!=null){ lk.lock(); }
        }
    
        private void unlockInternal()
        {
            if(lk!=null){ lk.unlock(); }
        }
    
        public List<String> getTags(){
            List<String> ref = null;            
            lockInternal();
                [...] //internal operations
            unlockInternal();
            return ref;
        }
    
        public void setTags(List<String> tags){
            [...] // similar approach that in getTags
        }
    
        public void addTags(List<String> tags){
            [...] // similar approach that in getTags
        }
    
        public void addTag(String tag){
            [...] // similar approach that in getTags
        }
    }
    

    原始答案:

    杰夫的答案是一个很好的起点,因为它似乎解决了触摸A型物体并修改其构成的问题。但是,我们仍然遇到与A中包含的B型对象相关的问题。 假设我们有一个A型对象,里面有几个B型元素:

    A a1 = new A("10");
    B b1 = new B("myB1");
    B b2 = new B("myB2");
    B b3 = new B("myB3");
    a1.add(b1);
    a1.add(b2);
    a1.add(b3);
    

    问题是,如果您使用a1进行操作,则需要锁定b1b2b3上的任何操作。你怎么能确定这个?

    我看到的唯一解决方案是A容器内的所有B元素共享一些常见的Lock变量。在a1上调用与写入相关的操作时,它获取锁定,避免对B元素的任何操作。请注意,对B元素的操作不需要实际获取锁定...仅检查它是否已被A型容器获取。

    许多缺点/考虑因素:

    • 性能:B对象上的每个操作都涉及检查锁(这可能非常昂贵)
    • 操作限制:如果可以将单个B对象添加到多个A型容器中,则会变得更加复杂
    • 实现复杂性:在a1中插入B对象时,必须为B提供共享锁。解压缩后,必须取消引用锁定。如果容器A被删除会怎么样?你必须照顾那种事情。

    编辑:我忘了提到这种机制,在编写A型容器时,不会“停止”或考虑对所包含的B型元素的持续操作。