Mehrere Threads, die FutureTask aufrufen, haben dazu geführt, dass die Aufrufmethode des von uns geschriebenen Callable nur einmal ausgeführt wurde (Grund: detaillierte Erklärung des Quellcodes) (Problemlösung: detaillierte Erklärung)

Zunächst wissen wir, dass FutureTask die RunnableFuture-Schnittstelle implementiert und die RunnableFuture-Schnittstelle die Runnable-Schnittstelle und die Future-Schnittstelle erbt.

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

Es handelt sich also um eine Anpassungsklasse, die für die Verwendung von Callable in new Thread() verantwortlich ist. Nutzen Sie die Idee des Adapterentwurfsmusters .

Okay, schauen wir uns den folgenden Code an:

public class FutureTaskTest {
    public static void main(String[] args) throws Exception {
        FutureTask<String> task=new FutureTask<>(()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥";
        });
        new Thread(task).start();
        new Thread(task).start();
        System.out.println(task.get());
    }
}

Operationsergebnis:

run...
Thread-0
小贱哥哥

Zu diesem Zeitpunkt haben wir ein Problem entdeckt: Ich habe offensichtlich zwei Threads zur Ausführung verwendet. Warum wurde das Ergebnis nur einmal ausgegeben?

Was ist los?

Tatsächlich ist das Problem sehr einfach. Schauen wir uns zunächst den von uns geschriebenen Code an und analysieren ihn anhand des Quellcodes:

FutureTask<String> task=new FutureTask<>(()->{
    System.out.println("run...");
    System.out.println(Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(1);
    return "小贱哥哥";
});

Wir haben eine neue FutureTask erstellt und die entsprechende FutureTask wurde intern ausgeführt:

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
}

Achten wir auf: this.state = NEW; und this.callable = callable;

Dann beginnen wir mit der Ausführung:

new Thread(task).start();
new Thread(task).start();

Lassen Sie uns zunächst über eine Frage nachdenken. Da FutureTask die Runnable-Schnittstelle indirekt implementiert, muss die Ausführungsmethode in FutureTask neu geschrieben werden. Daher ist es besser, die Ausführungsmethode von FutureTask zu verwenden, wenn der Thread mit der Ausführung beginnt . Schauen wir uns das an seine Laufmethode:

public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
    }
}

Wir sehen , dass es nur möglich ist, die Call-Methode auszuführen (  result = c.call(); ), wenn state == NEW und callable ! = null ist.

(Der Inhalt dieser c.call(); -Methode ist das, was wir zuvor geschrieben haben) (hier wird der Lambda-Ausdruck verwendet):

FutureTask<String> task=new FutureTask<>(()->{
    System.out.println("run...");
    System.out.println(Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(1);
    return "小贱哥哥";
});

Okay, los geht's, denn unser Programm läuft normal bis zum Ende, also gehen wir weiter nach unten, bis zur Set-Methode ( immer noch die Run-Methode oben ):

public void run() {

        ......

        if (ran)
           set(result);

        ......
}

Dann werfen wir einen Blick auf die Set-Methode:

protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
}

STATE.setRelease(this, NORMAL) gefunden ; er hat den Status in NORMAL geändert.

Wenn wir weiter in der Set-Methode nach unten schauen, um die Methode „finishCompletion“ zu sehen , klicken wir darauf und wir finden:

private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (WAITERS.weakCompareAndSet(this, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }

        done();

        callable = null;        // to reduce footprint
}

Diese Methode ändert schließlich unser Callable in null .

Okay, hier ist alles klar.

Wenn der erste Thread ausgeführt wird , ist der Status von FutureTask nicht mehr NEU und das Callable wird null. Und da unsere beiden Threads dieselbe Instanz von FutureTask verwenden , kehrt der zweite Thread bei der Ausführung direkt hier zurück:

public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    
    .......

}

Deshalb wird es nur einmal ausgeführt.

Wir wollen das nicht, wie können wir diese Situation korrigieren?

Tatsächlich müssen wir nur einen Weg finden, den Wert von „state“ auf „NEW“ zu setzen und den Wert von „callable“ nicht auf „null“ zu setzen.

Methode 1: Erstellen Sie eine neue FutureTask:

((Denn bei der Erneuerung wird er die Konstruktionsmethode der FutureTask verwenden, um den Status in NEU zu ändern und Callable neu zuzuweisen.) Ich habe dies bereits zuvor erwähnt: this.state = NEW; und this.callable = callable; )

public class FutureTaskTest {
    public static void main(String[] args) throws Exception{
        final int[] i={0};
        Callable callable=()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥"+(++i[0]);
        };
        FutureTask<String> task1=new FutureTask<>(callable);
        FutureTask<String> task2=new FutureTask<>(callable);
        new Thread(task1).start();
        new Thread(task2).start();
        System.out.println(task1.get());
        System.out.println(task2.get());
    }
}

Das Ergebnis ist dieses Mal:

run...
Thread-1
run...
Thread-0
小贱哥哥2
小贱哥哥1

Methode 2: Denken Sie zuerst nach: Die run()-Methode von FutureTask wird durch die Tatsache verursacht, dass der Status von FutureTask nicht NEU ist und der Aufruf von FutureTask null ist . Gibt es eine Möglichkeit, diese beiden Werte zu ändern? Schauen wir uns den Quellcode an:

private volatile int state;
private Callable<V> callable;

Wir haben festgestellt, dass sie alle privat sind und wir Änderungen nicht direkt aufrufen können. Was sollen wir tun? (Antwort: Reflexion verwenden )

public class FutureTaskTest {
    public static void main(String[] args) throws Exception {
        final int[] i = {0};
        FutureTask<String> task=new FutureTask<>(()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥"+(++i[0]);
        });
        new Thread(task).start();
        Field callable = task.getClass().getDeclaredField("callable");
        callable.setAccessible(true);
        Object call = callable.get(task);
        System.out.println(task.get());
        Field state = task.getClass().getDeclaredField("state");
        state.setAccessible(true);
        state.set(task,0);
        callable.set(task,call);
        new Thread(task).start();
        System.out.println(task.get());
    }
}

Ergebnis:

run...
Thread-0
小贱哥哥1
run...
Thread-1
小贱哥哥2

Sie sind möglicherweise verwirrt über state.set(task,0); oben , warum sollte es auf 0 gesetzt werden?

Wir müssen uns also noch den Quellcode ansehen:

private volatile int state;
private static final int NEW          = 0; //任务新建和执行中
private static final int COMPLETING   = 1; //任务将要执行完毕
private static final int NORMAL       = 2; //任务正常执行结束
private static final int EXCEPTIONAL  = 3; //任务异常
private static final int CANCELLED    = 4; //任务取消
private static final int INTERRUPTING = 5; //任务线程即将被中断
private static final int INTERRUPTED  = 6; //任务线程已中断

Vermutlich sollten Sie jetzt sehr gründlich vorgehen.

Okay, noch eine letzte Sache: Die Leute haben es absichtlich so entwickelt, und der Zweck besteht darin, die Effizienz zu verbessern . Da der Rückgabewert immer derselbe ist, müssen wir immer darüber nachdenken, ihn zwischenzuspeichern und den Wert beim nächsten Mal direkt zurückzugeben. Sie müssen keine Berechnungen mehr durchführen, was die Effizienz erheblich verbessert.

Ich hoffe, es hilft dir!

Ich denke du magst

Origin blog.csdn.net/xiaomaomixj/article/details/126821063
Empfohlen
Rangfolge