Java-已同步,但允许不同线程访问一种方法

时间:2018-12-14 17:47:18

标签: java

在下面的示例中:

=
可以同时访问

inc1和inc2,但是不能同时由多个线程访问。

怎么可能只允许访问inc1或Inc2,而另一个却像常规同步一样,但是却允许通过尽可能多的线程来访问被访问的一个。

2 个答案:

答案 0 :(得分:2)

我认为一个有用的比喻是通过交叉路口的交通,在这里您可以让多辆汽车共享一条路,只要它们平行行驶即可。面临的挑战是找到一种交通交叉的协调策略。

如果流量是间歇性的,@ Greg提出的解决方案将起作用,我们可以等待一个流停止,然后再允许相交的流继续进行。但是我怀疑这不是很现实。如果某条路上的路况稳定,其余的汽车将永远等待,也就是线程饥饿。

另一种策略是允许汽车以先到先得的方式越过,就像停车标志一样。我们可以针对每个用户在其中获得许可的“道路”或路段使用专用的semaphore来实现,首先要确保其他路段均未使用许可:

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

要使用该类,我们只需调用enter(i)exit(i),其中i标识我们要使用的道路/路段/方法。这是一个使用3个细分的演示:

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

在我的计算机上运行的测试会产生此交替模式:

120201210012012210102120021021

如果流量相对较小,则此策略可能有意义,但在流量较大时,协调每个交叉口的开销可能会严重限制吞吐量。对于繁忙的十字路口,通常需要一个交通信号灯,或者一个可以以合理的频率转移控制权的第三方。这是这种概念的实现,使用管理一组read/write locks的后台线程,确保一次只有一个段具有写锁:

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

现在让我们调整测试代码:

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

结果是有序的模式:

000111112222200000111112222200

注意:

  • awaitUninterruptibly()sleepUninterruptibly()Guava helper methods,以避免处理InterruptedException。如果您不想导入库,请随时复制implementation
  • TrafficLight可以通过将状态管理委托给访问线程来实现,而不是依赖于后台线程。这种实现较为简单(我认为),但确实有一些额外的开销,并且需要shutdown()进行垃圾收集。
  • 为了方便起见,测试代码使用并行流,但是根据您的环境,它可能不会很好地交织。您始终可以使用适当的线程代替。

答案 1 :(得分:1)

您可以跟踪所处的模式以及正在进行的该类型的操作的数量,然后仅在所有这些操作完成后才翻转模式,例如:

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