Explain the members of the AQS family: Semaphore

Follow: Wang Youzhi , a mutual gold fisherman who shares hard-core Java technology.
Welcome to join the bucket-carrying group of Java people : Java people who get rich together

Today we will talk about Semaphore, another important member of the AQS family. I only collected one interview question about Semaphore, asking "what" and "how to achieve it":

  • What are Semaphore? How is it achieved?

According to our practice, we still analyze Semaphore according to the three steps of "what", "how to use" and "how to implement". In addition, problem solutions are provided today .

Use of Semaphore

Semaphore is literally translated as a semaphore , which is a very Old School mechanism for processing synchronization and mutual exclusion in computer science . Unlike a mutex, it allows a specified number of threads or processes to access shared resources .

Semaphore's mechanism for handling synchronization and mutual exclusion is very similar to the gates we usually pass through subway stations. Swipe the card to open the gate ( acquireoperation ), after passing ( access critical area ), the gate is closed ( releaseoperation ), and the people behind can continue to swipe the card, but before the previous person passes, the people behind can only wait in line ( queue mechanism ). Of course, it is impossible for a subway station to have only one gate. With several gates, several people are allowed to pass through at the same time.

insert image description here

The same is true for semaphores. Define the number of licenses through the constructor, apply for a license when using it, and release the license after processing the business logic:

// 信号量中定义1个许可
Semaphore semaphore = new Semaphore(1);

// 申请许可
semaphore.acquire();

......

// 释放许可
semaphore.release();

When we define a permission for a Semaphore , it is the same as a mutex, allowing only one thread to enter the critical section at a time . But when we define multiple permissions, it differs from mutexes:

Semaphore semaphore = new Semaphore(3);
for(int i = 1; i < 5; i++) {
  int finalI = i;
  new Thread(()-> {
    try {
      semaphore.acquire();
      System.out.println("第[" + finalI + "]个线程获取到semaphore");
      TimeUnit.SECONDS.sleep(10);
      semaphore.release();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }).start();
}

Execute this code and you can see that all three threads have entered the critical section at the same time, and only the fourth thread is blocked outside the critical section.

The realization principle of Semaphore

Remember the synchronization state mentioned in " AQS's present life, building the foundation of JUC " ? We were saying it was a counter for some synchronizer:

In AQS, state is not only used to indicate the synchronization state, but also a counter implemented by some synchronizers , such as: the number of threads allowed to pass in Semaphore, and the realization of reentrant features in ReentrantLock, all depend on statethe features as counters.

Let's first look at the relationship between Semaphore and AQS:

insert image description here

Like ReentrantLock, Semaphore internally implements the synchronizer abstract class inherited from AQS Sync, and has FairSynctwo NonfairSyncimplementation classes. Next, we will verify our previous statement by analyzing the source code of Semaphore.

Construction method

Semaphore provides two constructors:

public Semaphore(int permits) {
  sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
  sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

It can be seen that the design ideas of Semaphore and ReentrantLock are consistent. Semaphore also implements two synchronizers FairSyncand NonfairSyncimplements the fair mode and the unfair mode respectively. The construction of Semaphore is essentially the realization of the construction of the synchronizer. Let's take the implementation of the unfair mode NonfairSyncas an example:

public class Semaphore implements java.io.Serializable {
  static final class NonfairSync extends Sync {
    NonfairSync(int permits) {
      super(permits);
    }
  }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
      setState(permits);
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  protected final void setState(int newState) {
    state = newState;
  }
}

Tracing back to the source, the parameters of the constructor permitsare finally returned to AQS state, and statethe function of Semaphore is realized by using the characteristics of a counter.

acquire method

Now that we have set a certain number of permissions (permits) for the Semaphore, we need to Semaphore#acquireobtain the permission through the method and enter the critical section "guarded" by the Semaphore:

public class Semaphore implements java.io.Serializable {
  public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    if (tryAcquireShared(arg) < 0) {
      doAcquireSharedInterruptibly(arg);
    }
  }
}

These two steps are very similar to ReentrantLock. First, try to obtain the license directly, and then add it to the waiting queue tryAcquireSharedafter failure .doAcquireSharedInterruptibly

The logic of obtaining permission directly in Semaphore is very simple:

static final class NonfairSync extends Sync {
  protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
  }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
  final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
      // 获取可用许可数量
      int available = getState();
      // 计算许可数量
      int remaining = available - acquires;
      if (remaining < 0 || compareAndSetState(available, remaining)) {
        return remaining;
      }
    }
  }
}

The first is to obtain and reduce the number of available licenses. When the number of licenses is less than 0, a negative number is returned, or after the number of licenses is successfully updated through CAS, a positive number is returned. At this time, doAcquireSharedInterruptiblythe current thread applying for the Semaphore license will be added to the waiting queue of AQS.

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {    
 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
   // 创建共享模式的等待节点
   final Node node = addWaiter(Node.SHARED);
   try {
     for (;;) {
       final Node p = node.predecessor();
       if (p == head) {
         // 再次尝试获取许可,并返回剩余许可数量
         int r = tryAcquireShared(arg);
         if (r >= 0) {
           // 获取成功,更新头节点
           setHeadAndPropagate(node, r);
           p.next = null;
           return;
         }
       }
       // 获取失败进入等待状态
       if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
         throw new InterruptedException();
       }
     }
   } catch (Throwable t) {
     cancelAcquire(node);
     throw t;
   }
 }
}

The core logic of Semaphore is the same doAcquireSharedInterruptiblyas the method ReentrantLockused acquireQueued, but there are subtle implementation differences:

  • Create a node usage Node.SHAREDpattern;

  • Updating the head node uses setHeadAndPropagatethe method.

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head;
  setHead(node);
  
  // 是否要唤醒等待中的节点
  if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared()) {
      // 唤醒等待中的节点
      doReleaseShared();
    }
  }
}

We know that it is executed in ReentrantLock acquireQueued. After the lock is successfully acquired, it only needs to be executed setHead(node), so why does Semaphore wake up again?

Suppose there are 3 licensed Semaphore with T1, T2, T3 and T4 competing for a total of 4 threads at the same time:

  • They enter nonfairTryAcquireSharedthe method at the same time, assuming that only T1 compareAndSetState(available, remaining)successfully modifies the number of valid permits, and T1 enters the critical section;

  • T2, T3 and T4 enter doAcquireSharedInterruptiblythe method, and addWaiter(Node.SHARED)build the waiting queue of AQS (refer to the analysis of the method in the present life of AQSaddWaiter );

  • Assuming that T2 becomes the direct successor node of the head node, T2 executes tryAcquireSharedto try to obtain permission again, and T3 and T4 execute parkAndCheckInterrupt;

  • T2 successfully obtains the license and enters the critical section. At this time, Semaphore has 1 license left, while T3 and T4 are in the suspended state.

In this scenario, only two licenses work, which is obviously not in line with our original intention. Therefore, when updating the setHeadAndPropagatehead node, judge the number of remaining licenses, and continue to wake up the successor nodes when the number is greater than 0.

Tips

  • Semaphore's process of obtaining permission is highly similar to the process of ReentrantLock locking~~

  • The following analysis doReleaseSharedis how to wake up the waiting node.

release method

The release method of Semaphore is very simple:

public class Semaphore implements java.io.Serializable {
  public void release() {
    sync.releaseShared(1);
  }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    protected final boolean tryReleaseShared(int releases) {
      for (;;) {
        int current = getState();
        // 计算许可数量
        int next = current + releases;
        if (next < current) {
          throw new Error("Maximum permit count exceeded");
        }
        // 通过CAS更新许可数量
        if (compareAndSetState(current, next)) {
            return true;
        }
      }
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
      doReleaseShared();
      return true;
    }
    return false;
  }
  
  private void doReleaseShared() {
    for (;;) {
      Node h = head;
      // 判断AQS的等待队列是否为空
      if (h != null && h != tail) {
        int ws = h.waitStatus;
        // 判断当前节点是否处于待唤醒的状态
        if (ws == Node.SIGNAL) {
          if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)){
            continue;
          }
          unparkSuccessor(h);
        } else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) {
          // 状态为0时,更新节点的状态为无条件传播
          continue;
        }
      }
      if (h == head) {
        break;
      }
    }
  }
}

We can see that Semaphore's releasemethod is divided into two parts:

  • tryReleaseSharedThe method updates the number of valid licenses for the Semaphore;

  • doReleaseSharedWake up waiting nodes.

The logic of the wake-up is not complicated. It is still the judgment of the node status waitStatusto determine whether it needs to be executed unparkSuccessor. When the status is ws == 0, the status of the node will be updated to Node.PROPAGAT, that is, unconditional propagation.

Tips : Unlike ReentrantLock, Semaphore does not support Node.CONDITIONstate, and the same ReentrantLock does not support Node.PROPAGATEstate.

epilogue

This is the end of the content about Semaphore. Today we only specifically analyzed the implementation of the core method in the unfair mode. As for the implementation of the fair mode and other methods, we will leave it to you to explore on your own.

Well, I hope this article can bring you some help, see you next time! Finally, everyone is welcome to pay attention to Wang Youzhi 's column " What do Java interviews ask?" ".

Guess you like

Origin blog.csdn.net/wyz_1945/article/details/131097692
AQS
AQS