有時候,我們希望提前結束線程,但安全可靠地停止線程,並不是一件容易的事情,如果立即停止線程,會使共享的數據結構處於不一致的狀態,如目前已經廢棄使用的Thread類的stop方法(它會使線程在拋出java.lang.ThreadDeath之後終止線程,即使是在執行synchronized方法的時候)。更好的做法是執行完終止處理,再終止線程,即Two-phase Termination,兩階段終止模式。
該模式有兩個角色:
- Terminator,終止者,負責接收終止請求,執行終止處理,處理完成後再終止自己。
- TerminationRequester:終止請求發出者,用來向Terminator發出終止請求。
該模式示例代碼如下:
Terminator:
public class CounterIncrement extends Thread {
private volatile boolean terminated = false;
private int counter = 0;
private Random random = new Random(System.currentTimeMillis());
@Override
public void run() {
try {
while (!terminated) {
System.out.println(Thread.currentThread().getName()+" "+counter++);
Thread.sleep(random.nextInt(1000));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.clean();
}
}
private void clean() {
System.out.println("do some clean work for the second phase,current counter "+counter);
}
public void close() {
this.terminated = true;
this.interrupt();
}
}
TerminationRequester:
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
CounterIncrement counterIncrement = new CounterIncrement();
counterIncrement.start();
Thread.sleep(15_000L);
//主動清理
counterIncrement.close();
}
}
這段代碼可以看出實現兩階段終止模式必須注意的是:
使用線程停止標誌和interrupt方法,兩者缺一不可
public void close() {
this.terminated = true;
this.interrupt();
}
這裏使用了terminated作爲線程停止標誌,變量採用volatile修飾,避免了使用顯式鎖的開銷,又保證了內存可見性。線程run方法會檢查terminated屬性,如果屬性爲true,就停止線程,但線程可能調用了阻塞方法,處於wait狀態,任務也就可能永遠不會檢查terminated標誌;線程也有可能處於sleep()狀態,等sleep時間過後再執行終止狀態,程序的響應性就下降了。你可以把方法改成如下運行,線程停止明顯變慢了許多:
public void close() {
terminated = true;
}
模擬客戶端或者服務端都可能終止服務的例子
public class AppServer extends Thread {
private static final int DEFAULT_PORT = 12722;
private final static ExecutorService executor = Executors.newFixedThreadPool(10);
private int port;
private volatile boolean start = true;
private List<ClientHandler> clientHandlers = new ArrayList<>();
private ServerSocket server;
public AppServer() {
this(DEFAULT_PORT);
}
public AppServer(int port) {
this.port = port;
}
@Override
public void run() {
try {
server = new ServerSocket(port);
while (start) {
Socket client = server.accept();
ClientHandler clientHandler = new ClientHandler(client);
executor.submit(clientHandler);
this.clientHandlers.add(clientHandler);
}
} catch (IOException e) {
//throw new RuntimeException();
} finally {
this.dispose();
}
}
public void dispose() {
System.out.println("dispose");
this.clientHandlers.stream().forEach(ClientHandler::stop);
this.executor.shutdown();
}
public void shutdown() throws IOException {
this.start = false;
this.interrupt();
this.server.close();
}
}
public class ClientHandler implements Runnable {
private final Socket socket;
private volatile boolean running = true;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
PrintWriter printWriter = new PrintWriter(outputStream)) {
while (running) {
String message = br.readLine();
if (message == null) {
break;
}
System.out.println("Come from client >" + message);
printWriter.write("echo " + message+"\n");
printWriter.flush();
}
} catch (IOException e) {
//自動關閉的時候 將running
this.running = false;
}finally {
this.stop();
}
}
public void stop() {
if (!running) {
return;
}
this.running = false;
try {
this.socket.close();
} catch (IOException e) {
}
}
}
public class AppServerClient {
public static void main(String[] args) throws InterruptedException, IOException {
AppServer server = new AppServer(12135);
server.start();
Thread.sleep(20_000L);
server.shutdown();
}
}
mac telnet模擬客戶端輸入
bogon:~ kpioneer$ telnet localhost 12135
Trying ::1...
Connected to localhost.
Escape character is '^]'.
hello
echo hello
I love you
echo I love you
Connection closed by foreign host.
服務端輸出:
Come from client >hello
Come from client >I love you
dispose
總結:
可以看到,在子類使用兩階段終止模式時,其只需要實現各自所需要執行的任務,並且更新當前任務的數量即可。在某些情況下,當前任務的數量也可以不進行更新,比如在進行終止時,不關心當前剩餘多少任務需要執行。