dawnwords
dawnwords

Reputation: 45

Use CAS instead of synchronized

Recently, I came across a situation when creating a factory producing reusable instances.

public class Factory {
    private static final int REUSABLE_TIMES_LIMIT = 10;
    private static Product instance = new Product();
    private static int getTimes;

    public static synchronized Product getInstance() {
        if (++getTimes >= REUSABLE_TIMES_LIMIT) {
            return nextInstance();
        }
        return instance;
    }

    public static synchronized Product nextInstance() {
        getTimes = 0;
        instance = new Product();
        return instance;
    }
}

Since getInstance() and nextInstance() might both be invoked concurrently by different threads in my case, I choose to add synchronized key words before each of them. However, synchronized is too heavy when lots of threads comes to the method, so I'd like to rewrite this class based on CAS, i.e. those classes in the package of java.util.concurrent.atomic. Unfortunately, I didn't figure out a proper way to arrange my code with two atomic variables, namely instance and getTimes, in the same time. Will someone show me how to correctly use CAS instead of synchronized without causing race condition in this situation? Thanks in advance :)

Upvotes: 1

Views: 321

Answers (1)

Andrey Cheboksarov
Andrey Cheboksarov

Reputation: 646

The one possible option is to use one AtomicReference instead of two. This will make your state consistent regardless of the code compexity.

public static class ProductStorage {
    private Product product;
    private int getTimes;

    public ProductStorage(Product product, int getTimes) {
        this.product = product;
        this.getTimes = getTimes;
    }
}

public static class Factory {
    private static final int REUSABLE_TIMES_LIMIT = 10;
    private static AtomicReference<ProductStorage> instance = new AtomicReference<>(
            new ProductStorage(new Product(), 0)
    );

    public static Product getInstance() {
        ProductStorage current;
        for(;;) {
            current = instance.get();
            if(current.getTimes >= REUSABLE_TIMES_LIMIT) {

                instance.compareAndSet(current, new ProductStorage(new Product(), 0));
                continue;
            }
            if(current.getTimes < REUSABLE_TIMES_LIMIT) {

                if(instance.compareAndSet(current, new ProductStorage(current.product, current.getTimes + 1))) {
                    return current.product;
                }
            }
        }
    }
}

The first thing you may mention is that new object is always allocated in that case. But remember that most of lock-free algorithms do that and it's not a problem. Allocation in java is fast and costs a few nanoseconds. You may also see similar solution in Martin Thompson's blog. The code is here. On my machine lock-free solution runs 3-4 times fastrer.

If may want to go with two atomics, but that will make counting of getTimes hard.

Upvotes: 1

Related Questions