Ben
Ben

Reputation: 345

Java - Synchronized but allow one method to be accessed by different threads

In the example below:

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }      

    public void inc2() {
        synchronized(lock2) {
            c2++;      
        }
    }
}

inc1 and inc2 can be accessed at the same time, but neither can be accessed by multiple threads at the same time.

How would it be possible to allow only inc1 or inc2 to be accessed whilst the other is like regular syncing however allowing the one that is being accessed to be done so by as many threads as possible.

Upvotes: 2

Views: 525

Answers (2)

shmosel
shmosel

Reputation: 50726

I think a useful analogy is traffic passing through an intersection, where you can have multiple cars sharing one road, as long as they're driving in parallel. The challenge is finding a coordination strategy for intersecting traffic.

The solution proposed by @Greg works if traffic is intermittent and we can wait for one stream to stop before allowing the intersecting stream to proceed. But I suspect that's not very realistic. If there's steady traffic on one road, the rest of the cars will wait forever, a.k.a. thread starvation.

An alternative strategy is to allow cars to cross on a first come, first served basis, like at a stop sign. We can implement that using a dedicated semaphore for each "road", or segment, where each user takes a permit, after first making sure none of the other segments have permits in use:

public class StopSign {
    private final Semaphore[] locks;
    private volatile int current = 0;

    public StopSign(int segments) {
        // create and populate lock array, leaving
        // all segments drained besides the first
        locks = new Semaphore[segments];
        Arrays.setAll(locks, i -> new Semaphore(i == 0 ? Integer.MAX_VALUE : 0, true));
    }

    public void enter(int segment) {
        // synchronization is necessary to guard `current`,
        // with the added benefit of holding up new threads
        // in the active segment while we're gathering permits
        synchronized (locks) {
            if (segment == current) {
                // if our segment is active, acquire a permit
                locks[segment].acquireUninterruptibly();
            } else {
                // otherwise, gather all permits from the active segment
                // as they become available and then reclaim our own permits
                locks[current].acquireUninterruptibly(Integer.MAX_VALUE);
                current = segment;
                locks[segment].release(Integer.MAX_VALUE - 1);
            }
        }
    }

    public void exit(int segment) {
        if (segment != current) {
            // we don't own the lock!
            throw new IllegalMonitorStateException();
        }
        locks[segment].release();
    }
}

To use the class, we simply call enter(i) and exit(i), where i identifies the road/segment/method we want to use. Here's a demo using 3 segments:

public static void main(String args[]) {
    int segments = 3;
    StopSign lock = new StopSign(segments);
    IntStream.range(0, segments).parallel().forEach(i -> {
        for (int j = 0; j < 10; j++) {
            lock.enter(i);
            System.out.print(i);
            lock.exit(i);
            sleepUninterruptibly(20, TimeUnit.MILLISECONDS);
        }
    });
}

A test run on my machine produces this alternating pattern:

120201210012012210102120021021

This strategy could make sense if traffic is relatively light, but in heavy traffic the overhead of coordinating each crossing can significantly restrict throughput. For busy intersections, you'll usually want a traffic light, or a third party that can transfer control at a reasonable frequency. Here's an implementation of a such a concept, using a background thread that manages a set of read/write locks, making sure only one segment has a write lock available at a time:

public class TrafficLight {
    private final ReadWriteLock[] locks;
    private final Thread changer;

    public TrafficLight(int segments, long changeFrequency, TimeUnit unit) {
        // create and populate lock array
        locks = new ReadWriteLock[segments];
        Arrays.setAll(locks, i -> new ReentrantReadWriteLock(true));

        CountDownLatch initialized = new CountDownLatch(1);
        changer = new Thread(() -> {
            // lock every segment besides the first
            for (int i = 1; i < locks.length; i++) {
                locks[i].writeLock().lock();
            }
            initialized.countDown();

            int current = 0;
            try {
                while (true) {
                    unit.sleep(changeFrequency);
                    // lock the current segment and cycle to the next
                    locks[current].writeLock().lock();
                    current = (current + 1) % locks.length;
                    locks[current].writeLock().unlock();
                }
            } catch (InterruptedException e) {}
        });
        changer.setDaemon(true);
        changer.start();

        // wait for the locks to be initialized
        awaitUninterruptibly(initialized);
    }

    public void enter(int segment) {
        locks[segment].readLock().lock();
    }

    public void exit(int segment) {
        locks[segment].readLock().unlock();
    }

    public void shutdown() {
        changer.interrupt();
    }
}

Now let's tweak the test code:

TrafficLight lock = new TrafficLight(segments, 100, TimeUnit.MILLISECONDS);

The result is an orderly pattern:

000111112222200000111112222200

Notes:

  • awaitUninterruptibly() and sleepUninterruptibly() are Guava helper methods to avoid handling InterruptedException. Feel free to copy the implementation if you don't want to import the library.
  • TrafficLight could be implemented by delegating state management to visiting threads, instead of relying on a background thread. This implementation is simpler (I think), but it does have some extra overhead and it requires a shutdown() to be garbage collected.
  • The test code uses parallel streams for convenience, but depending on your environment, it may not interleave very well. You can always use proper threads instead.

Upvotes: 2

Greg
Greg

Reputation: 191

You could keep track of what mode you're in, and how many operations of that type are in progress, then only flip the mode when all of those operations are complete, eg:

public class MsLunch {
  private enum LockMode {IDLE, C1_ACTIVE, C2_ACTIVE};
  private LockMode lockMode = IDLE:
  private int activeThreads = 0;
  private long c1 = 0;
  private long c2 = 0;

  public void inc1() {
    try {
      enterMode(C1_ACTIVE);
      c1++
    } finally {
      exitMode();
    }
  }

  public void inc2() {
    try {
      enterMode(C2_ACTIVE);
      c2++
    } finally {
      exitMode();
    }
  }

  private synchronized void enterMode(LockMode newMode){
    while(mode != IDLE && mode != newMode) {
      try {
        this.wait(); // don't continue while threads are busy in the other mode
      } catch(InterruptedException e) {}
    }
    mode = newMode;
    activeThreads++;
  }

  private synchronized void exitMode(){
    activeThreads--;
    if (activeThreads == 0) {
      mode = IDLE;
      this.notifyAll(); // no more threads in this mode, wake up anything waiting
    }
  }
}

Upvotes: 1

Related Questions