จะหยุดการเธรดใน Java อย่างถูกต้องได้อย่างไร?


276

ฉันต้องการวิธีแก้ปัญหาเพื่อหยุดเธรดใน Java อย่างถูกต้อง

ฉันมีIndexProcessorคลาสที่ใช้ส่วนต่อประสาน Runnable:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);

    @Override
    public void run() {
        boolean run = true;
        while (run) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                run = false;
            }
        }

    }
}

และฉันมีServletContextListenerชั้นเรียนที่เริ่มต้นและหยุดเธรด:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        thread = new Thread(new IndexProcessor());
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (thread != null) {
            thread.interrupt();
            LOGGER.debug("Thread successfully stopped.");
        }
    }
}

แต่เมื่อฉันปิด Tomcat ฉันได้รับข้อยกเว้นในคลาส IndexProcessor ของฉัน:

2012-06-09 17:04:50,671 [Thread-3] ERROR  IndexProcessor Exception
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at lt.ccl.searchengine.processor.IndexProcessor.run(IndexProcessor.java:22)
    at java.lang.Thread.run(Unknown Source)

ฉันใช้ JDK 1.6 ดังนั้นคำถามคือ:

ฉันจะหยุดเธรดและไม่โยนข้อยกเว้นใด ๆ ได้อย่างไร

PSฉันไม่ต้องการใช้.stop();วิธีการเพราะเลิกใช้แล้ว


1
การยุติเธรดครึ่งทางจะสร้างข้อยกเว้นเสมอ InterruptedExceptionถ้ามันเป็นพฤติกรรมปกติแล้วคุณก็สามารถจับและไม่สนใจ นี่คือสิ่งที่ฉันคิด แต่ฉันก็ยังสงสัยว่าวิธีมาตรฐานคืออะไร
nhahtdh

ฉันไม่ได้ใช้เธรดบ่อยนักดังนั้นฉันจึงค่อนข้างใหม่กับเธรดดังนั้นฉันไม่ทราบว่าเป็นพฤติกรรมปกติที่จะละเว้นข้อยกเว้นหรือไม่ นั่นคือเหตุผลที่ฉันถาม
Paulius Matulionis

ในหลายกรณีมันเป็นพฤติกรรมปกติที่จะละเว้นข้อยกเว้นและยุติการประมวลผลวิธีการ ดูคำตอบของฉันด้านล่างเพื่อดูว่าทำไมสิ่งนี้ถึงดีกว่าวิธีการตั้งค่าสถานะ
Matt

1
คำอธิบายที่เรียบร้อยโดยบีเก๊เกี่ยวโยงInterruptedExceptionสามารถพบได้ที่ibm.com/developerworks/library/j-jtp05236
แดเนียล

InterruptedException ไม่ใช่ปัญหาปัญหาเดียวของคุณในโค้ดที่โพสต์คือคุณไม่ควรบันทึกมันเป็นข้อผิดพลาดไม่มีเหตุผลที่น่าสนใจจริงๆที่จะบันทึกมันทั้งหมดยกเว้นว่าเป็น debug เพียงเพื่อแสดงให้เห็นว่ามันเกิดขึ้นในกรณีที่คุณสนใจ . คำตอบที่เลือกนั้นโชคไม่ดีเพราะไม่อนุญาตให้ตัดสายสั้น ๆ เช่นการรอสายและรอสาย
Nathan Hughes

คำตอบ:


173

ในIndexProcessorชั้นเรียนคุณต้องมีวิธีการตั้งค่าสถานะซึ่งแจ้งเธรดที่จะต้องยกเลิกคล้ายกับตัวแปรrunที่คุณใช้ในขอบเขตของคลาส

เมื่อคุณต้องการหยุดเธรดคุณตั้งค่าสถานะนี้และเรียกใช้join()บนเธรดและรอให้เสร็จสิ้น

ตรวจสอบให้แน่ใจว่าการตั้งค่าสถานะเป็นเธรดที่ปลอดภัยโดยใช้ตัวแปร volatile หรือโดยใช้วิธี getter และ setter ซึ่งซิงโครไนส์กับตัวแปรที่ใช้เป็นค่าสถานะ

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);
    private volatile boolean running = true;

    public void terminate() {
        running = false;
    }

    @Override
    public void run() {
        while (running) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                running = false;
            }
        }

    }
}

จากนั้นในSearchEngineContextListener:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;
    private IndexProcessor runnable = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        runnable = new IndexProcessor();
        thread = new Thread(runnable);
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (thread != null) {
            runnable.terminate();
            thread.join();
            LOGGER.debug("Thread successfully stopped.");
        }
    }
}

3
ฉันทำเช่นเดียวกับที่คุณให้ตัวอย่างในคำตอบของคุณก่อนที่ฉันจะได้เห็นว่าคุณแก้ไขมัน คำตอบที่ดี! ขอขอบคุณคุณตอนนี้ทุกอย่างทำงานได้อย่างสมบูรณ์ :)
Paulius Matulionis

1
จะเกิดอะไรขึ้นถ้าตรรกะของเธรดนั้นซับซ้อนและเรียกใช้เมธอดของคลาสอื่น ๆ มากมาย? ไม่สามารถตรวจสอบสถานะบูลีนได้ทุกที่ ถ้าอย่างนั้นจะทำอย่างไร?
Soteric

คุณจะต้องเปลี่ยนการออกแบบรหัสเพื่อให้คุณสร้างมันในลักษณะที่สัญญาณไปยัง Runnable จะทำให้เธรดออกจาก การใช้งานส่วนใหญ่จะมีการวนซ้ำในวิธีการเรียกใช้จึงมักจะไม่เกิดปัญหา
DrYap

3
จะเกิดอะไรขึ้นถ้าคำสั่ง join () ส่งข้อผิดพลาด InterruptedException?
benzaita

14
ลงคะแนนเพื่อเผยแพร่คำแนะนำที่ไม่ดี วิธีตั้งค่าสถานะการรีดด้วยมือหมายความว่าแอปพลิเคชันต้องรอให้การนอนหลับเสร็จสิ้นซึ่งการหยุดชะงักจะทำให้การนอนหลับสั้นลง มันจะง่ายต่อการแก้ไขเพื่อใช้ Thread # interrupt
Nathan Hughes

298

การใช้Thread.interrupt()เป็นวิธีที่ยอมรับได้อย่างสมบูรณ์แบบในการทำเช่นนี้ ในความเป็นจริงมันอาจจะดีกว่าการตั้งค่าสถานะตามที่แนะนำข้างต้น เหตุผลที่ว่าถ้าคุณอยู่ในการโทรที่บล็อกไม่ได้ (เช่นThread.sleepหรือใช้การดำเนินการช่อง java.nio) คุณจะสามารถแยกสิ่งเหล่านั้นออกได้ทันที

หากคุณใช้การตั้งค่าสถานะคุณต้องรอให้การดำเนินการบล็อกเสร็จสิ้นจากนั้นคุณสามารถตรวจสอบการตั้งค่าสถานะของคุณ ในบางกรณีคุณต้องทำเช่นนี้เช่นการใช้มาตรฐานInputStream/ OutputStreamซึ่งไม่สามารถขัดจังหวะได้

ในกรณีนั้นเมื่อเธรดถูกขัดจังหวะเธรดจะไม่ขัดจังหวะ IO อย่างไรก็ตามคุณสามารถทำสิ่งนี้ได้เป็นประจำในรหัสของคุณ (และคุณควรทำเช่นนี้ที่จุดยุทธศาสตร์ที่คุณสามารถหยุดและล้างได้อย่างปลอดภัย)

if (Thread.currentThread().isInterrupted()) {
  // cleanup and stop execution
  // for example a break in a loop
}

ดังที่ฉันได้กล่าวไปข้อได้เปรียบหลักThread.interrupt()คือคุณสามารถแยกการโทรออกที่ขัดจังหวะได้ทันทีซึ่งคุณไม่สามารถทำได้ด้วยวิธีการตั้งค่าสถานะ


32
1 - Thread.interupt () เป็นแน่นอนกว่าที่จะดำเนินการในสิ่งเดียวกันโดยใช้ธงเฉพาะกิจ
สตีเฟ่นซี

2
ฉันคิดว่านี่เป็นวิธีที่สมบูรณ์แบบและมีประสิทธิภาพในการทำเช่นนั้น +1
RoboAlex

4
มีการพิมพ์ผิดขนาดเล็กในรหัส Thread.currentThread () ไม่มีวงเล็บ
Vlad V

1
จริงๆแล้วมันไม่เหมาะที่จะใช้ธงเพราะคนอื่นที่เข้ามาติดต่อกับเธรดอาจขัดจังหวะมันจากที่อื่นทำให้มันหยุดและยากที่จะดีบั๊ก ใช้ธงเสมอเช่นกัน
JohnyTex

ในกรณีนี้การโทรที่เฉพาะเจาะจงinterrupt()อาจจะใช้ได้ แต่ในหลายกรณีอื่น ๆ มันไม่ได้เป็น (ตัวอย่างเช่นถ้าจำเป็นต้องปิดทรัพยากร) หากใครเปลี่ยนการทำงานภายในของลูปคุณจะต้องจำไว้ว่าให้เปลี่ยนinterrupt()เป็นบูลีน ฉันจะไปเพื่อความปลอดภัยตั้งแต่ต้นและใช้ธง
m0skit0

25

คำตอบง่ายๆ: คุณสามารถหยุดเธรดภายในได้ด้วยวิธีใดวิธีหนึ่งจากสองวิธี:

  • เมธอด run ส่งผลให้รูทีนย่อย return
  • วิธีการเรียกใช้เสร็จสิ้นและส่งกลับโดยปริยาย

นอกจากนี้คุณยังสามารถหยุดกระทู้ภายนอก:

  • โทร system.exit (นี้จะฆ่ากระบวนการทั้งหมดของคุณ)
  • เรียกวัตถุของเธรด interrupt()วิธีการ *
  • ดูว่าเธรดมีวิธีการใช้งานที่ฟังดูเหมือนจะใช้งานได้ไหม (เช่นkill()หรือstop())

*: ความคาดหวังคือสิ่งนี้ควรหยุดเธรด อย่างไรก็ตามสิ่งที่เธรดทำจริงเมื่อเกิดเหตุการณ์นี้ขึ้นอยู่กับสิ่งที่นักพัฒนาเขียนเมื่อสร้างการใช้เธรด

รูปแบบทั่วไปที่คุณเห็นกับการใช้งานวิธีการทำงานเป็นwhile(boolean){}ที่บูลโดยทั่วไปจะมีบางสิ่งบางอย่างที่มีชื่อisRunningก็เป็นตัวแปรสมาชิกของชั้นด้ายของมันก็มีความผันผวนและมักจะสามารถเข้าถึงได้โดยหัวข้ออื่น ๆ kill() { isRunnable=false; }โดยวิธีการตั้งค่าของทุกประเภทเช่น รูทีนย่อยเหล่านี้ดีเพราะอนุญาตให้เธรดปล่อยรีซอร์สใด ๆ ที่เก็บไว้ก่อนที่จะยกเลิก


3
"รูทีนย่อยเหล่านี้ดีเพราะอนุญาตให้เธรดปล่อยทรัพยากรใด ๆ ที่เก็บไว้ก่อนที่จะยกเลิก" ฉันไม่เข้าใจ คุณสามารถล้างทรัพยากรของเธรดได้อย่างสมบูรณ์โดยใช้สถานะขัดจังหวะ "อย่างเป็นทางการ" เพียงตรวจสอบโดยใช้ Thread.currentThread (). isInterrupted () หรือ Thread.interrupted () (แล้วแต่ความต้องการของคุณ) หรือจับ InterruptedException และล้างข้อมูล ปัญหาอยู่ที่ไหน
Franz D.

ฉันไม่สามารถเข้าใจว่าทำไมวิธีการตั้งค่าสถานะเพราะฉันไม่เข้าใจว่าจะหยุดเมื่อมีการเรียกใช้ผลตอบแทน !!! นี่เป็นเรื่องง่ายมากท่านที่รักขอบคุณสำหรับการชี้ให้เห็นว่าไม่มีใครทำอย่างนี้อย่างชัดเจน
thahgr

9

คุณควรสิ้นสุดเธรดเสมอโดยตรวจสอบการตั้งค่าสถานะในการrun()วนรอบ (ถ้ามี)

หัวข้อของคุณควรมีลักษณะเช่นนี้:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);
    private volatile boolean execute;

    @Override
    public void run() {
        this.execute = true;
        while (this.execute) {
            try {
                LOGGER.debug("Sleeping...");
                Thread.sleep((long) 15000);

                LOGGER.debug("Processing");
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
                this.execute = false;
            }
        }
    }

    public void stopExecuting() {
        this.execute = false;
    }
}

thread.stopExecuting()จากนั้นคุณสามารถจบด้ายโดยการเรียก วิธีนี้จะทำให้เธรดสะอาด แต่ใช้เวลาสูงสุด 15 วินาที (เนื่องจากคุณนอนหลับ) คุณยังสามารถเรียกใช้เธรดขัดจังหวะ () หากเป็นเรื่องเร่งด่วน แต่วิธีที่ต้องการควรตรวจสอบการตั้งค่าสถานะเสมอ

เพื่อหลีกเลี่ยงการรอเป็นเวลา 15 วินาทีคุณสามารถแบ่งการนอนหลับดังนี้:

        ...
        try {
            LOGGER.debug("Sleeping...");
            for (int i = 0; (i < 150) && this.execute; i++) {
                Thread.sleep((long) 100);
            }

            LOGGER.debug("Processing");
        } catch (InterruptedException e) {
        ...

2
มันไม่ใช่Thread- มันใช้Runnable- คุณไม่สามารถเรียกThreadวิธีการต่าง ๆ ได้เว้นแต่ว่าคุณจะประกาศThreadในกรณีที่คุณไม่สามารถโทรได้stopExecuting()
Don Cheadle

7

โดยทั่วไปเธรดจะถูกยกเลิกเมื่อถูกขัดจังหวะ ดังนั้นทำไมไม่ใช้บูลีนดั้งเดิม ลองใช้ isInterrupted ():

Thread t = new Thread(new Runnable(){
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                // do stuff         
            }   
        }});
    t.start();

    // Sleep a second, and then interrupt
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {}
    t.interrupt();

ref- ฉันจะฆ่าเธรดได้อย่างไร โดยไม่ใช้ stop ();


5

สำหรับการซิงโครไนซ์เธรดฉันต้องการใช้CountDownLatchซึ่งจะช่วยให้เธรดรอจนกว่ากระบวนการที่กำลังดำเนินการเสร็จสมบูรณ์ ในกรณีนี้คลาสคนงานถูกตั้งค่าด้วยCountDownLatchอินสแตนซ์ที่มีการนับที่กำหนด การเรียกawaitใช้เมธอดจะบล็อกจนกว่าจำนวนปัจจุบันจะถึงศูนย์เนื่องจากการเรียกใช้countDownเมธอดหรือถึงการหมดเวลาที่ตั้งไว้ วิธีการนี้อนุญาตให้ขัดจังหวะเธรดได้ทันทีโดยไม่ต้องรอเวลารอที่ระบุเพื่อผ่านไป:

public class IndexProcessor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(IndexProcessor.class);

    private final CountDownLatch countdownlatch;
    public IndexProcessor(CountDownLatch countdownlatch) {
        this.countdownlatch = countdownlatch;
    }


    public void run() {
        try {
            while (!countdownlatch.await(15000, TimeUnit.MILLISECONDS)) {
                LOGGER.debug("Processing...");
            }
        } catch (InterruptedException e) {
            LOGGER.error("Exception", e);
            run = false;
        }

    }
}

เมื่อคุณต้องการดำเนินการของเธรดอื่นให้ดำเนินการ countDown บนCountDownLatchและjoinเธรดไปยังเธรดหลัก:

public class SearchEngineContextListener implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchEngineContextListener.class);

    private Thread thread = null;
    private IndexProcessor runnable = null;
    private CountDownLatch countdownLatch = null;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        countdownLatch = new CountDownLatch(1);
        Thread thread = new Thread(new IndexProcessor(countdownLatch));
        LOGGER.debug("Starting thread: " + thread);
        thread.start();
        LOGGER.debug("Background process successfully started.");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        LOGGER.debug("Stopping thread: " + thread);
        if (countdownLatch != null) 
        {
            countdownLatch.countDown();
        } 
        if (thread != null) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                LOGGER.error("Exception", e);
            }
            LOGGER.debug("Thread successfully stopped.");
        } 
    }
}

3

ข้อมูลเสริมบางอย่าง แนะนำให้ตั้งค่าสถานะและอินเตอร์รัปต์ใน Java doc

https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

private volatile Thread blinker;

public void stop() {
    blinker = null;
}

public void run() {
    Thread thisThread = Thread.currentThread();
    while (blinker == thisThread) {
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e){
        }
        repaint();
    }
}

สำหรับเธรดที่รอเป็นเวลานาน (เช่นสำหรับอินพุต) ให้ใช้ Thread.interrupt

public void stop() {
     Thread moribund = waiter;
      waiter = null;
      moribund.interrupt();
 }

3
อย่าเพิกเฉย InterruptException หมายความว่ามีบางรหัสอื่นที่ขอให้เธรดของคุณยุติอย่างชัดเจน เธรดที่ละเว้นการร้องขอนั้นคือเธรดที่หลอกลวง วิธีที่ถูกต้องในการจัดการกับ InterruptedException คือออกจากลูป
VGR

2

ฉันไม่ได้ขัดจังหวะการทำงานใน Android ดังนั้นฉันจึงใช้วิธีนี้ทำงานได้อย่างสมบูรณ์แบบ:

boolean shouldCheckUpdates = true;

private void startupCheckForUpdatesEveryFewSeconds() {
    threadCheckChat = new Thread(new CheckUpdates());
    threadCheckChat.start();
}

private class CheckUpdates implements Runnable{
    public void run() {
        while (shouldCheckUpdates){
            System.out.println("Do your thing here");
        }
    }
}

 public void stop(){
        shouldCheckUpdates = false;
 }

นี่คือแนวโน้มที่จะล้มเหลวเพราะ shouldCheckUpdates volatileไม่ ดูdocs.oracle.com/javase/specs/jls/se9/html/jls-17.html#jls-17.3
VGR

0

บางครั้งฉันจะลอง 1,000 ครั้งใน onDestroy () / contextDestroyed ()

      @Override
    protected void onDestroy() {
        boolean retry = true;
        int counter = 0;
        while(retry && counter<1000)
        {
            counter++;
            try{thread.setRunnung(false);
                thread.join();
                retry = false;
                thread = null; //garbage can coll
            }catch(InterruptedException e){e.printStackTrace();}
        }

    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.