简体   繁体   中英

Object deep locking in multithreading environment

I have below class structures in my project for class A and class 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);
    }
}

Also we have a cache class:

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

Now objects for class A and B are created using unique IDs and put in this cache by <id, object> combination. For example:

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

Also I am putting class B objects in the List<B> inside objects a1 and a2 . It is important to note that a unique B object is only put once in any A object :

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

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

This way we can have multiple objects for class A and B in the CACHE.

Scenario: Now multiple threads access this CACHE but some of them end up to get class A objects and others class B objects depending upon ID specified by user. These threads actually want to read or update information on these objects.

Question: My requirement is when a thread has accessed an object of class A (for example a1 ) to update it then no other thread should be able to read or update a1 as well as all class B objects ( b1 and b2 in this case) which are added to the List<B> inside object a1 until I am finished with all updates on a1 . Please tell me how can I acquire a lock in this scenario?

To have deep copying I suggest you to have a Lock in each instance of A and B and have them both implement some interface with lock() and unlock() methods, where class A will acquire its own lock, and all locks of B's . Then, lock an object before using it, and unlock after you're done.

EDIT: So your course of action would be:

  1. create an interface, let's call it Lockable with two methods: lock() and unlock()
  2. have both A and B implement that interface. Accordingly, your cache will now operate with Lockable instead of Object
  3. add a private field to both A and B

    private final Lock lock = new ReentrantLock ();

  4. now implementation of Lockable in B will be just to call same methods on the lock
  5. in A, lock() will acquire local instance of lock, and iterate the list of b's and call their lock() method as well. Same for unlock()
  6. now, every time you get an object from your cache, before doing anything with it, you call lock() on that object and then unlock() when you're done.

The synchronized keyword will help you with this. You can either declare an entire method as synchronized or you can have synchronized() blocks, which lock on a specific key object which you define. When a synchronized() block is entered, no other blocks which lock with that same key object can be accessed by another thread until the block is exited.

See the Java tutorial on synchronization here .

For your example, you could do either of the following:

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

OR

... declare a lock object in your class ...

private final Object LOCK = new Object();

... and use it a synchronized() block:

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

The advantages to using the second over the first is that you can completely, explicitly control which sections of code are locked (rather than just the entire method call). When dealing with concurrency, you want to synchronize as little as possible for efficiency purposes, so using this you can perform synchronization on only the bare minimum.

Also, I'm sure someone will point out that you don't need to explicitly declare another lock object, because you are able to synchronize on the current object using the this keyword. However, this other StackOverflow question and answer sum up the reasons I would not recommend doing so nicely.

EDIT2: I was going to complete my answer after question was edited, but I just found the class java.util.concurrent.locks.ReentrantReadWriteLock , which I think fits exactly the needs of user sohanlal.

This class provides a pair of locks, one for reading operations that can be owned at the same time by several elements and is not-blocking by itself. The second one (the write part) avoids any read or write operation when acquired.

The solution is then as following:

  1. Add lock to container class A. This class is the true owner of the read-write lock.
  2. Add lock attribute to class B. This class DOES NOT own the lock, but only references it to read-lock it.
  3. When adding a new B object to an A container, provide the B object with a reference to the ReentrantReadWriteLock on the container. When extracting a B object from A, dereference the lock "inherited" from the container.
  4. In class A, write-lock on each operation that modifies the collection of B objects (add, removew...). Optionally (if underlying collection of B elements is not synchronized), read-lock operations that do not modify the collection but access it.
  5. In class B, read-lock on every operation.

Code. This has not been tested, but will give an idea of how it should be implemented:

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

ORIGINAL ANSWER:

Jeff's answer is a good starting point, since it appears to solve the problem of touching the A-type object, and modifying its composition. However, we still have a problem related with theB-type objects contained on A. Suppose we have an A-type object with several B-type elements inside:

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

The thing is that, if you are operating over a1 , you want to lock any operation over b1 , b2 and b3 . How can you make sure of that?

The only solution I see is that all the B elements inside an A container share some common Lock variable. At the time of invoking write-related operations over a1 , it acquires the lock, avoiding ANY operation over B elements. Note that no operation over a B element needs to actually acquire the lock... only to check if it has been acquired by A-type container.

A lot of drawbacks/considerations:

  • Performance: every operation over a B object will involve checking the lock (this can be very costly)
  • Operating constraints: if a single B object can be added to several A-type containers, this gets much more complicated
  • Implementation complexity: when you insert a B object inside a1 , you have to provide B with the shared lock. When extracted, the lock must be dereferenced. What happens if container A is deleted? You have to take care of that kind of things.

EDIT : and I forgot to mention that this mechanism, when writing the A-type container, does not "stop" or take into account ongoing operations over the contained B-type elements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM