You want to share an object container across different threads? Then you automatically also share the stored objects across threads, due to the reference-cache. Therefore you need to synchronize the access to your object-model.
The basic access-pattern should be like this:
For example we want to do some back-ground updates, while the rest of the application carries on. As soon as we have this kind pattern we need to protect the access to the data model.
For example this operation starts a background task and carries on doing other things:
// Schedule back-ground tasks final Future<?> task = executor.submit(new Runnable() { @Override public void run() { updateSomePeople(container); } }); // While doing other work listAllPeople(container);
Unfortunately these two tasks do work on the same data model, which can lead to race conditions. Therefore access to the data model has to be protected.
private void updateSomePeople(ObjectContainer container) { synchronized (dataLock) { final ObjectSet<Person> people = container.query(new Predicate<Person>() { @Override public boolean match(Person person) { return person.getName().equals("Joe"); } }); for (Person joe : people) { joe.setName("New Joe"); container.store(joe); } } }
private void listAllPeople(ObjectContainer container) { synchronized (dataLock) { for (Person person : container.query(Person.class)) { System.out.println(person.getName()); } } }
Of course the locking showed above is very course grained. A simple improvement would be to use read-write locks. In the end you need to adapt the locking strategy to your application.
One model which works good for this scenario is to create a transaction abstraction to do your operations on the data model. Then you do all operations in such a transaction. The transaction manages the lock and the db4o transaction. Such an implementation could look like this:
public <T> T inTransaction(TransactionFunction<T> transactionClosure) { synchronized (lock) { try { return transactionClosure.inTransaction(database); } catch (Exception e) { database.rollback(); throw new TransactionFailedException(e.getMessage(), e); } finally { database.commit(); } } }
And then we can use this transaction method when accessing our data model.
private void listAllPeople(DatabaseSupport dbSupport) { dbSupport.inTransaction(new TransactionAction() { @Override public void inTransaction(ObjectContainer container) { final ObjectSet<Person> result = container.query(Person.class); for (Person person : result) { System.out.println(person.getName()); } } }); }
private void updateAllJoes(DatabaseSupport dbSupport) { dbSupport.inTransaction(new TransactionAction() { @Override public void inTransaction(ObjectContainer container) { final ObjectSet<Person> allJoes = container.query(new Predicate<Person>() { @Override public boolean match(Person person) { return person.getName().equals("Joe"); } }); for (Person joe : allJoes) { joe.setName("New Joe"); container.store(joe); } } }); }
Remember that this is only an example. You can use other techniques like annotations or aspects to implement the right behavior. And you also can use more sophisticated locks, like read write locks. The only thing which is important is that you synchronize the access your shared data objects.
Alternatively you can avoid sharing data objects and rather use multiple object container to manage concurrent data access.