Regardez de plus près le code source de Condition dans AQS

Allez-y, aujourd'hui, je veux juste parler du code source de ConditionObject, pas beaucoup de potins. Le mot ConditionObject peut sembler un peu étrange, mais vous êtes toujours familier avec Condition, non? Dans ReentrantLock, condition.await () et condition.signal () sont généralement utilisés pour réaliser le mécanisme d'attente et de réveil du verrou.

Regardons d'abord un exemple simple, cela peut être un casse-tête de regarder directement le code source.

public class WaitTest {

    private static Lock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {

        // 线程一
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                //to do sth
                System.out.println("线程一开始");
                condition.await();
                System.out.println("线程一等待后继续运行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread1");
        //线程二
        Thread thread2 = new Thread(() -> {
            try {
                //线程二等待下,让线程一先开始
                Thread.sleep(20);
                lock.lock();
                //获取锁,再等待,线程一已经进入等待队列,只能等线程二
                Thread.sleep(200);
                //to do sth
                System.out.println("线程二开始");
                //唤醒了线程一,但是没有释放锁,即使释放锁了,线程一抢占时间片也需要时间,未必有线程二先执行完
                condition.signal();
                System.out.println("线程二等待后继续运行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Thread2");

        thread1.start();
        thread2.start();
    }
}

Jetez un œil aux résultats de l'exécution:

线程一开始
线程二开始
线程二等待后继续运行
线程一等待后继续运行

Jetons un coup d'œil à ce programme, une fonction principale qui crée deux threads asynchrones, utilise le verrou exclusif ReentrantLock et utilise la condition d'attente pour le mécanisme de réveil dans ReentrantLock en même temps.

Tout d'abord, le thread un et le thread deux saisissent le verrou.Bien sûr, le thread deux dort un court instant, ce qui équivaut à abandonner le droit d'utiliser le verrou et ne peut être bloqué que dans la file d'attente de synchronisation.

Ensuite, dès que le thread attend, il entre dans la file d'attente et abandonne la propriété du verrou.

Thread deux préempte le verrou et commence à s'exécuter vers le bas.

Le thread deux appelle alors condition.signal () pour réveiller le thread un dans la file d'attente. Le thread un entre dans la file d'attente de synchronisation depuis la file d'attente. Cependant, le verrou est toujours occupé par le thread deux ! Thread on ne peut que continuer à attendre.

Bien sûr, même si le thread deux libère le contrôle du verrou, après condition.signal (), exécutez lock.unlock (), le thread deux s'exécutera avant le thread 1. Après tout, le thread un se déplace vers la file d'attente de synchronisation et saisit le verrou. Cela prend du temps et le thread deux passe directement dessus.

Une autre chose à mentionner est qu'après avoir appelé condition.signal (), cela ne signifie pas que le thread réveillé peut être exécuté! Il suffit de mettre le thread dans la file d'attente de synchronisation, c'est juste et prêt. À ce stade, le thread doit encore obtenir le contrôle du verrou , c'est-à-dire qu'il doit être finalement alloué à la tranche de temps avant de pouvoir être exécuté. Cela nécessite que tout le monde y prête attention. Le thread en Java peut être exécuté immédiatement après qu'il ne soit pas réveillé!

En ce qui concerne la condition, nous devons comprendre deux étapes:

1. Relâchez le verrou et entrez dans la file d'attente

2. Réveillez le verrou, sortez de la file d'attente et déplacez à nouveau le thread vers la file d'attente de synchronisation.

Jetons un œil au code source:

 /* lock 锁等待方法 */ 
 public final void await() throws InterruptedException {
			//线程是中断的,没法等待,抛异常
            if (Thread.interrupted())
                throw new InterruptedException();
			// 新建一个节点,并添加到等待队列中,这个时候是占有lock锁的
            Node node = addConditionWaiter();
			// 释放当前线程占有的lock
            int savedState = fullyRelease(node);
            int interruptMode = 0;
			//是否在同步队列中
            while (!isOnSyncQueue(node)) {
				//阻塞当前
                LockSupport.park(this);
				//checkInterruptWhileWaiting(node)) 不等于0,跳出,这个是唤醒之后的操作。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
			// 自旋等待获取到同步状态(即获取到lock)
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
		

Le morceau de code ci-dessus est la logique principale de l'attente d'attente, parlons du mécanisme principal d'attendre le réveil en verrou. Tout d'abord, nous devons savoir que si le nœud de thread est déplacé vers la file d'attente, ce thread doit maintenir le verrou à ce moment.

Examinons la première étape. Le code a une explication. Permettez-moi d'en parler. Premièrement, le thread actuel détient un verrou. Le thread actuel crée un nouveau nœud de nœud et l'ajoute à la fin de la file d'attente. Cette file d'attente est une FIFO en attente. Une fois la file d'attente ajoutée à la file d'attente, le verrou est libéré et l'occupation des ressources est terminée. Mais pour le moment, le thread n'est pas directement bloqué, mais continue de s'exécuter, mais les ressources verrouillées ont été libérées. Nous pouvons voir que FullyRelease (node); le code est toujours en cours d'exécution ci-dessous.

Puis une boucle while est exécutée. Puisqu'il s'agit d'une boucle while, elle sautera définitivement. Il est impossible de boucler indéfiniment, non? Tout d'abord, si la condition while est fausse, elle ne sera naturellement pas exécutée à nouveau, et il y a un signe "rupture" dans le corps de la boucle. Si la rupture est exécutée, elle sautera naturellement!

C'est la première fois de juger s'il se trouve dans la file d'attente de synchronisation (! IsOnSyncQueue (node)). Il ne doit pas y être. Juste déménagé, où est-il? Entrez ensuite le corps de la boucle, exécutez LockSupport.park (this) ;, puis restez bloqué. À ce stade, la première étape est complètement terminée ~

Jetons un coup d'œil à une partie du code source à l'intérieur.

la première! Ajoutez un nouveau nœud à la file d'attente.

  private Node addConditionWaiter() {
			//将lastWaiter赋值给t
            Node t = lastWaiter;
            // 节点在等待队列上,但是状态又不是Node.CONDITION,那状态就是CANCELLED(1)了,
			// 已经取消调度,直接可以清除了
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
			//根据当前进程,新建一个节点。
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
		

Ce n'est pas difficile, il suffit d'ajouter un nœud à la file d'attente, je crois que tout le monde peut le comprendre, (* ^ ▽ ^ *). Permettez-moi de poster l'un des appels à l'intérieur

	 /* 取消已取消的服务程序节点 */
   private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
			//首节点不为空,进入循环,就是为了干掉所有滴
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

Ce qui suit est un morceau de code plus critique, libérez le verrou

 final int fullyRelease(Node node) {
		 //失败标志
        boolean failed = true;
        try {
			// 获取当前线程的state
            int savedState = getState();
			//释放锁,调用的lock.release
            if (release(savedState)) {
				// 释放成功,返回state
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
			// 释放失败,那就直接让这个线程节点跪了呗。
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

Ce code consiste à abandonner le contrôle de la serrure, qui est similaire au code qui libère la serrure dans la serrure. La première consiste à obtenir l'état du thread, puis à libérer. Une fois la libération exécutée, le contrôle du verrou est en fait abandonné. Le code suivant est un traitement du thread en cours. Après son retour, il sera finalement exécuté à LockSupport. Park (this); Bloquer le thread actuel.

De cette façon, on peut essentiellement connaître un processus d'attente: établir un nœud d'attente --- "entrer dans la file d'attente ---" libérer le verrou --- "bloc en attente

Parlons de la deuxième étape, le signal-réveil.

Pour réveiller le thread, nous devons clarifier deux points: Premièrement, le thread actuel doit obtenir le verrou avant de pouvoir être réveillé. Deuxièmement, le réveil ne peut pas être spécifié. La file d'attente est un FIFO et seul le thread qui entre en premier dans la file d'attente peut être réveillé.

D'accord, jetons un œil au code.

	// 唤醒最早进入等待队列的线程
	 public final void signal() {
			// 是否拥有锁,没有就抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
			// 获取第一个节点
            Node first = firstWaiter;
            if (first != null)
				// 不为空就唤醒
                doSignal(first);
        }
	
	   // 是否独占锁,公平锁的实现方式
	   protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

Le code source ci-dessus n'est rien à contourner. Les opérations de réveil sont toutes dans doSignal (en premier); Dans cette opération, allons-y et jetons un œil.

	 // 唤醒等待线程
	  private void doSignal(Node first) {
            do {
				// 等待队列首节点干掉
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
				
            }
			//成功就加入到同步队列,结束循环,否则就继续找下一个节点
			while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
		
 
    final boolean transferForSignal(Node node) {
        // 重新设置waitStatus,设置失败,说明已经cancel了,会在循环中干掉
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        //下面的代码是不是很熟悉?嘿嘿,没错,就是加入到同步队列中去的操作
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

Jetons un œil au code de doSignal. Premièrement, récupérez le premier nœud firstWaiter et supprimez ce nœud de la file d'attente. Exécutez ensuite l'opération transferForSignal. Si la mise à jour de l'état aboutit, passez à la file d'attente de synchronisation et mettez fin à la boucle. Si le nœud a été annulé, continuez à rechercher le nœud suivant jusqu'à ce que le premier nœud encore actif dans la file d'attente soit réveillé.

Et pour tout réveiller? En fait, c'est très simple: ce qui précède ne se réveille-t-il pas et ne termine-t-il pas la boucle? Je ne suis plus hors de la boucle, puis-je continuer à me réveiller? \ (^ o ^) / ~, montrez-moi ~

 // 唤醒所有的等待队列
	  private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
				//移出等待队列,添加到同步队列
                transferForSignal(first);
                first = next;
            } 
			//首节点不为空就一直循环
			while (first != null);
        }

Le code de base est juste quelques lignes, est-ce facile?

En résumé, le thread détient le verrou ---- "récupère le premier nœud de la file d'attente ----" sort de la file d'attente ---- "ajoute à la file d'attente de synchronisation

L'idée n'est pas difficile à comprendre. Si le code est implémenté, je l'ai posté. S'il y a une erreur, vous pouvez la signaler dans les commentaires ~

De plus, s'il est inconfortable de regarder directement le code source, vous pouvez essayer de déboguer la méthode principale de mon test ci-dessus, et la logique principale peut être atteinte ~

Pas de sacrifice , pas de victoire ~

Je suppose que tu aimes

Origine blog.csdn.net/zsah2011/article/details/111665592
conseillé
Classement