La bibliothèque de classes elle-même contient de nombreuses classes avec des dépendances d'état. Tels que FutureTask, BlockingQueue, etc. Certaines opérations de ces classes sont basées sur des conditions préalables d'état. Par exemple, vous ne pouvez pas supprimer des éléments d'une file d'attente vide ou obtenir le résultat du calcul d'une tâche inachevée. Avant d'exécuter ces deux opérations, vous devez attendre que la file d'attente entre dans un état non vide ou que la tâche entre dans l'état terminé. Le moyen le plus simple pour nous de créer des classes dépendantes de l’état est de les construire sur la base d’une bibliothèque de classes. Mais si la bibliothèque de classes ne dispose pas des fonctionnalités souhaitées, vous pouvez également utiliser le mécanisme sous-jacent fourni par le langage Java et la bibliothèque de classes pour construire votre propre mécanisme de synchronisation.
Ainsi, cet article vous présentera comment construire votre propre classe de dépendances d'état. Présentez étape par étape la construction la plus simple à la construction standardisée complexe, afin de comprendre le processus et comment obtenir le résultat final.
L'opération de dépendance d'état bloquable est représentée dans le pseudo code suivant :
acquire lock on object state //首先获取锁 while (precondition does not hold) { //前提条件是否满足,不满足则一直循环重试 release lock //释放锁 wait until precondition might hold //等待知道满足前提条件 optionally fail if interrupted or timeout expire //中断或者超时,各种异常 reacquire lock //重新获取锁 } perform action //执行任务 release lock //释放锁
Acquérir le verrou et vérifier si les conditions sont remplies Si. non, puis relâchez le verrou et entrez dans l'état de blocage jusqu'à ce que les conditions soient remplies ou interrompues, délai d'attente, etc., et que le verrou soit réacquis. Exécutez la tâche et relâchez le verrou.
Vous ne pourrez peut-être pas le comprendre intuitivement lorsque vous regardez ce pseudocode maintenant. Ce n'est pas grave, continuez à lire et vous saurez ce que cela signifie après avoir lu cet article.
ArrayBlockingQueue est un cache limité qui fournit deux opérations, put et take. Ils contiennent tous une condition préalable : les éléments ne peuvent pas être placés dans un cache plein et les éléments ne peuvent pas être récupérés à partir d'un cache vide. Eh bien, notre objectif est de construire un tel ArrayBlockingQueue.
Ensuite, deux implémentations de cache limité sont présentées, qui utilisent des méthodes différentes pour gérer les situations où les conditions préalables ne sont pas remplies.
Tout d'abord, examinons la classe de base suivante, BaseBoundeBuffer. Les implémentations ultérieures étendent cette classe de base. Il s'agit d'un cache circulaire basé sur un tableau, et les variables buf, head, tail et count contenues sont protégées par le verrou intégré du cache. Il fournit également des méthodes doPut et doTake synchrones, et dans les sous-classes, les opérations put et take sont implémentées via ces méthodes, et l'état sous-jacent sera masqué aux sous-classes.
public abstract class BaseBoundedBuffer<V> { private final V[] buf; private int tail; private int head; private int count; protected BaseBoundedBuffer(int capacity) { this.buf = (V[]) new Object[capacity]; count = 0; } protected synchronized final void doPut(V v) { buf[tail] = v; if(++tail == buf.length) tail = 0; ++count; } protected synchronized final V doTake() { V v = buf[head]; buf[head] = null; if(++head == buf.length) head = 0; --count; return v; } public synchronized final boolean isFull() { return count == buf.length; } public synchronized final boolean isEmpty() { return count == 0; } }
La première implémentation du cache limité synchronise les méthodes put et take, vérifie d'abord puis s'exécute, et lève une exception en cas d'échec.
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer{ protected GrumpyBoundedBuffer(int capacity) { super(capacity); } public synchronized void put(V v) throws BufferFullException { if(isFull()) { throw new BufferFullException(); } doPut(v); } public synchronized V take() throws BufferFullException { if(isEmpty()) throw new BufferFullException(); return (V) doTake(); } }
Comme indiqué ci-dessus, si les conditions préalables ne sont pas remplies, une exception sera levée directement. La soi-disant exception ici fait référence au cache plein ou vide. En fait, cette exception ne signifie pas que le programme est erroné. Par exemple, voir un feu rouge ne signifie pas que le feu de signalisation est anormal, mais attendre que le feu vert traverse la route. Cela signifie donc que l'appelant doit intercepter l'exception et réessayer chaque opération de cache.
Regardons directement le code d'appel client suivant :
private static GrumpyBoundedBuffer gbb = new GrumpyBoundedBuffer(5); ...while(true) { try { V item = gbb.take(); break; } catch(BufferEmptyException e) { Thread.sleep(500); } }
Pour parler franchement, si les prérequis ne sont pas remplis, réessayez jusqu'à ce que les conditions soient remplies, pour que cela semble possible obtenir un effet de blocage. Mais dans ce cas, l'appelant doit gérer lui-même l'échec de la précondition et le CPU est toujours occupé. Le problème ici est qu'il sera très gênant pour l'appelant d'utiliser cette file d'attente !
La deuxième méthode, SleepyBoundedBuffer, implémente un mécanisme simple de nouvelle tentative de blocage via l'interrogation et la mise en veille, permettant à l'appelant de supprimer le mécanisme de nouvelle tentative et de simplifier l'utilisation du cache. Veuillez consulter la liste de codes suivante :
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer{ protected SleepyBoundedBuffer(int capacity) { super(capacity); // TODO Auto-generated constructor stub } public void put(V v) throws InterruptedException { while(true) { synchronized(this) { if(!isFull()) { doPut(v); return; } } Thread.sleep(200); } } public V take() throws InterruptedException{ while(true) { synchronized(this) { if(!isEmpty()) { return (V) doTake(); } } Thread.sleep(200); } } }
Du point de vue de l'appelant, cette méthode fonctionne bien. Si une opération remplit les prérequis, elle sera exécutée immédiatement, sinon elle sera bloquée. L'appelant n'a pas besoin de gérer les échecs et les tentatives, mais il doit toujours gérer InterruptedException. Comme la plupart des méthodes de bibliothèque de blocage efficaces, SleepyBoundedBuffer prend en charge l'annulation via des interruptions.
Le problème avec SleepyBoundedBuffer est le suivant : combien de temps le réglage du temps de sommeil est-il raisonnable ? Comment pouvons-nous atteindre des performances optimales ? Comme le montre la figure ci-dessous, le thread B définit la condition sur vrai, mais à ce moment-là, A dort toujours. Ce sommeil est le goulot d'étranglement des performances.
Eh bien, existe-t-il un moyen d'y parvenir ? Lorsque la condition est vraie, le thread se réveille immédiatement pour s'exécuter ?
Essayons, je vous l’explique dans le prochain article !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!