關(guān)閉線程的正確方法:“優(yōu)雅”的中斷

前文從任務(wù)到線程:Java結(jié)構(gòu)化并發(fā)應(yīng)用程序中介紹了如何安排任務(wù)啟動(dòng)線程。
線程在啟動(dòng)之后,正常的情況下會(huì)運(yùn)行到任務(wù)完成,但是有的情況下會(huì)需要提前結(jié)束任務(wù),如用戶取消操作等。可是,讓線程安全、快速和可靠地停止并不是件容易的事情,因?yàn)镴ava中沒有提供安全的機(jī)制來終止線程。雖然有Thread.stop/suspend等方法,但是這些方法存在缺陷,不能保證線程中共享數(shù)據(jù)的一致性,所以應(yīng)該避免直接調(diào)用。

線程在終止的過程中,應(yīng)該先進(jìn)行操作來清除當(dāng)前的任務(wù),保持共享數(shù)據(jù)的一致性,然后再停止。

慶幸的是,Java中提供了中斷機(jī)制,來讓多線程之間相互協(xié)作,由一個(gè)進(jìn)程來安全地終止另一個(gè)進(jìn)程。

1. 任務(wù)的取消

如果外部的代碼能在某個(gè)操作正常完成之前將其設(shè)置為完成狀態(tài),則該操作為可取消的Cancellable)。

操作被取消的原因有很多,比如超時(shí),異常,請(qǐng)求被取消等等。

一個(gè)可取消的任務(wù)要求必須設(shè)置取消策略,即如何取消,何時(shí)檢查取消命令,以及接收到取消命令之后如何處理。

最簡(jiǎn)單的取消辦法就是利用取消標(biāo)志位,如下所示:

public class PrimeGenerator implements Runnable {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    private final List<BigInteger> primes
            = new ArrayList<BigInteger>();
    //取消標(biāo)志位
    private volatile boolean cancelled;

    public void run() {
        BigInteger p = BigInteger.ONE;
        //每次在生成下一個(gè)素?cái)?shù)時(shí)堅(jiān)持是否取消
        //如果取消,則退出
        while (!cancelled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel() {
        cancelled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<BigInteger>(primes);
    }

    static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        exec.execute(generator);
        try {
            SECONDS.sleep(1);
        } finally {
            generator.cancel();
        }
        return generator.get();
    }
}

這段代碼用于生成素?cái)?shù),并在任務(wù)運(yùn)行一秒鐘之后終止。其取消策略為:通過改變?nèi)∠麡?biāo)志位取消任務(wù),任務(wù)在每次生成下一隨機(jī)素?cái)?shù)之前檢查任務(wù)是否被取消,被取消后任務(wù)將退出。

然而,該機(jī)制的最大的問題就是無法應(yīng)用于擁塞方法。假設(shè)在循環(huán)中調(diào)用了擁塞方法,任務(wù)可能因擁塞而永遠(yuǎn)不會(huì)去檢查取消標(biāo)志位,甚至?xí)斐捎肋h(yuǎn)不能停止。

1.1 中斷

為了解決擁塞方法帶來的問題,就需要使用中斷機(jī)制來取消任務(wù)。

雖然在Java規(guī)范中,線程的取消和中斷沒有必然聯(lián)系,但是在實(shí)踐中發(fā)現(xiàn):中斷是取消線程的最合理的方式

Thread類中和中斷相關(guān)的方法如下:

public class Thread {
    // 中斷當(dāng)前線程
    public void interrupt();
    // 判斷當(dāng)前線程是否被中斷
    public boolen isInterrupt();
    // 清除當(dāng)前線程的中斷狀態(tài),并返回之前的值
    public static boolen interrupt();   
}

調(diào)用Interrupt方法并不是意味著要立刻停止目標(biāo)線程,而只是傳遞請(qǐng)求中斷的消息。所以對(duì)于中斷操作的正確理解為:正在運(yùn)行的線程收到中斷請(qǐng)求之后,在下一個(gè)合適的時(shí)刻中斷自己。

使用中斷方法改進(jìn)素?cái)?shù)生成類如下:

public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            //使用中斷的方式來取消任務(wù)
            while (!Thread.currentThread().isInterrupted())
                //put方法會(huì)隱式檢查并響應(yīng)中斷
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            /* 允許任務(wù)退出 */
        }
    }

    public void cancel() {
        interrupt();
    }
}

代碼中有兩次檢查中斷請(qǐng)求:

  • 第一次是在循環(huán)開始前,顯示檢查中斷請(qǐng)求;
  • 第二次是在put方法,該方法為擁塞的,會(huì)隱式堅(jiān)持當(dāng)前線程是否被中斷;

1.2 中斷策略

和取消策略類似,可以被中斷的任務(wù)也需要有中斷策略:
即如何中斷,合適檢查中斷請(qǐng)求,以及接收到中斷請(qǐng)求之后如何處理。

由于每個(gè)線程擁有各自的中斷策略,因此除非清楚中斷對(duì)目標(biāo)線程的含義,否者不要中斷該線程。

正是由于以上原因,大多數(shù)擁塞的庫(kù)函數(shù)在檢測(cè)到中斷都是拋出中斷異常(InterruptedException)作為中斷響應(yīng),讓線程的所有者去處理,而不是去真的中斷當(dāng)前線程。

雖然有人質(zhì)疑Java沒有提供搶占式的中斷機(jī)制,但是開發(fā)人員通過處理中斷異常的方法,可以定制更為靈活的中斷策略,從而在響應(yīng)性和健壯性之間做出合理的平衡。

一般情況的中斷響應(yīng)方法為:

  1. 傳遞異常:收到中斷異常之后,直接將該異常拋出;
  2. 回復(fù)中斷狀態(tài):即再次調(diào)用Interrupt方法,恢復(fù)中斷狀態(tài),讓調(diào)用堆棧的上層能看到中斷狀態(tài)進(jìn)而處理它。

切記,只有實(shí)現(xiàn)了線程中斷策略的代碼才能屏蔽中斷請(qǐng)求,在常規(guī)的任務(wù)和庫(kù)代碼中都不應(yīng)該屏蔽中斷請(qǐng)求。中斷請(qǐng)求是線程中斷和取消的基礎(chǔ)。

1.3 定時(shí)運(yùn)行

定時(shí)運(yùn)行一個(gè)任務(wù)是很常見的場(chǎng)景,很多問題是很費(fèi)時(shí)間的,就需在規(guī)定時(shí)間內(nèi)完成,如果沒有完成則取消任務(wù)。

以下代碼就是一個(gè)定時(shí)執(zhí)行任務(wù)的實(shí)例:

public class TimedRun1 {
    private static final ScheduledExecutorService cancelExec = Executors.newScheduledThreadPool(1);

    public static void timedRun(Runnable r,
                                long timeout, TimeUnit unit) {
        final Thread taskThread = Thread.currentThread();
        cancelExec.schedule(new Runnable() {
            public void run() {
                // 中斷線程,
                // 違規(guī),不能在不知道中斷策略的前提下調(diào)用中斷,
                // 該方法可能被任意線程調(diào)用。
                taskThread.interrupt();
            }
        }, timeout, unit);
        r.run();
    }
}

很可惜,這是反面的例子,因?yàn)?em>timedRun方法在不知道Runnable對(duì)象的中斷策略的情況下,就中斷該任務(wù),這樣會(huì)承擔(dān)很大的風(fēng)險(xiǎn)。而且如果Runnable對(duì)象不支持中斷, 則該定時(shí)模型就會(huì)失效。

為了解決上述問題,就需要執(zhí)行任務(wù)都線程有自己的中斷策略,如下:

public class LaunderThrowable {
    public static RuntimeException launderThrowable(Throwable t) {
        if (t instanceof RuntimeException)
            return (RuntimeException) t;
        else if (t instanceof Error)
            throw (Error) t;
        else
            throw new IllegalStateException("Not unchecked", t);
    }
}

public class TimedRun2 {
    private static final ScheduledExecutorService cancelExec = newScheduledThreadPool(1);

    public static void timedRun(final Runnable r,
                                long timeout, TimeUnit unit)
            throws InterruptedException {
        class RethrowableTask implements Runnable {
            private volatile Throwable t;

            public void run() {
                try {
                    r.run();
                } catch (Throwable t) {
                    //中斷策略,保存當(dāng)前拋出的異常,退出
                    this.t = t;
                }
            }

            // 再次拋出異常
            void rethrow() {
                if (t != null)
                    throw launderThrowable(t);
            }
        }

        RethrowableTask task = new RethrowableTask();
        final Thread taskThread = new Thread(task);
        //開啟任務(wù)子線程
        taskThread.start();
        //定時(shí)中斷任務(wù)子線程
        cancelExec.schedule(new Runnable() {
            public void run() {
                taskThread.interrupt();
            }
        }, timeout, unit);

        //限時(shí)等待任務(wù)子線程執(zhí)行完畢
        taskThread.join(unit.toMillis(timeout));
        //嘗試拋出task在執(zhí)行中拋出到異常
        task.rethrow();
    }
}

無論Runnable對(duì)象是否支持中斷,RethrowableTask對(duì)象都會(huì)記錄下來發(fā)生的異常信息并結(jié)束任務(wù),并將該異常再次拋出。

1.4 通過Future取消任務(wù)

Future用來管理任務(wù)的生命周期,自然也可以來取消任務(wù),調(diào)用Future.cancel方法就是用中斷請(qǐng)求結(jié)束任務(wù)并退出,這也是Executor的默認(rèn)中斷策略。

用Future實(shí)現(xiàn)定時(shí)任務(wù)的代碼如下:

public class TimedRun {
    private static final ExecutorService taskExec = Executors.newCachedThreadPool();

    public static void timedRun(Runnable r,
                                long timeout, TimeUnit unit)
            throws InterruptedException {
        Future<?> task = taskExec.submit(r);
        try {
            task.get(timeout, unit);
        } catch (TimeoutException e) {
            // 因超時(shí)而取消任務(wù)
        } catch (ExecutionException e) {
            // 任務(wù)異常,重新拋出異常信息
            throw launderThrowable(e.getCause());
        } finally {
            // 如果該任務(wù)已經(jīng)完成,將沒有影響
            // 如果任務(wù)正在運(yùn)行,將因?yàn)橹袛喽蝗∠?            task.cancel(true); // interrupt if running
        }
    }
}

1.5 不可中斷的擁塞

一些的方法的擁塞是不能響應(yīng)中斷請(qǐng)求的,這類操作以I/O操作居多,但是可以讓其拋出類似的異常,來停止任務(wù):

  • Socket I/O: 關(guān)閉底層socket,所有因執(zhí)行讀寫操作而擁塞的線程會(huì)拋出SocketException
  • 同步 I/O:大部分Channel都實(shí)現(xiàn)了InterruptiableChannel接口,可以響應(yīng)中斷請(qǐng)求,拋出異常ClosedByInterruptException;
  • Selector的異步 I/O:Selector執(zhí)行select方法之后,再執(zhí)行closewakeUp方法就會(huì)拋出異常ClosedSelectorException

以套接字為例,其利用關(guān)閉socket對(duì)象來響應(yīng)異常的實(shí)例如下:

public class ReaderThread extends Thread {
    private static final int BUFSZ = 512;
    private final Socket socket;
    private final InputStream in;

    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = socket.getInputStream();
    }

    public void interrupt() {
        try {
            // 關(guān)閉套接字
            // 此時(shí)in.read會(huì)拋出異常
            socket.close();
        } catch (IOException ignored) {
        } finally {
            // 正常的中斷
            super.interrupt();
        }
    }

    public void run() {
        try {
            byte[] buf = new byte[BUFSZ];
            while (true) {
                int count = in.read(buf);
                if (count < 0)
                    break;
                else if (count > 0)
                    processBuffer(buf, count);
            }
        } catch (IOException e) { 
            // 如果socket關(guān)閉,in.read方法將會(huì)拋出異常
            // 借此機(jī)會(huì),響應(yīng)中斷,線程退出
        }
    }

    public void processBuffer(byte[] buf, int count) {
    }
}

2. 停止基于線程的服務(wù)

一個(gè)應(yīng)用程序是由多個(gè)服務(wù)構(gòu)成的,而每個(gè)服務(wù)會(huì)擁有多個(gè)線程為其工作。當(dāng)應(yīng)用程序關(guān)閉服務(wù)時(shí),由服務(wù)來關(guān)閉其所擁有的線程。服務(wù)為了便于管理自己所擁有的線程,應(yīng)該提供生命周期方來關(guān)閉這些線程。對(duì)于ExecutorService,其包含線程池,是其下屬線程的擁有者,所提供的生命周期方法就是shutdownshutdownNow方法。

如果服務(wù)的生命周期大于所創(chuàng)建線程的生命周期,服務(wù)就應(yīng)該提供生命周期方法來管理線程。

2.1 強(qiáng)行關(guān)閉和平緩關(guān)閉

我們以日志服務(wù)為例,來說明兩種關(guān)閉方式的不同。首先,如下代碼是不支持關(guān)閉的日志服務(wù),其采用多生產(chǎn)者-單消費(fèi)者模式,生產(chǎn)者將日志消息放入擁塞隊(duì)列中,消費(fèi)者從隊(duì)列中取出日志打印出來。

public class LogWriter {
    // 擁塞隊(duì)列作為緩存區(qū)
    private final BlockingQueue<String> queue;
    // 日志線程
    private final LoggerThread logger;
    // 隊(duì)列大小
    private static final int CAPACITY = 1000;

    public LogWriter(Writer writer) {
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.logger = new LoggerThread(writer);
    }

    public void start() {
        logger.start();
    }

    public void log(String msg) throws InterruptedException {
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        //線程安全的字節(jié)流
        private final PrintWriter writer;

        public LoggerThread(Writer writer) {
            this.writer = new PrintWriter(writer, true); // autoflush
        }

        public void run() {
            try {
                while (true)
                    writer.println(queue.take());
            } catch (InterruptedException ignored) {
            } finally {
                writer.close();
            }
        }
    }
}

如果沒有終止操作,以上任務(wù)將無法停止,從而使得JVM也無法正常退出。但是,讓以上的日志服務(wù)停下來其實(shí)并非難事,因?yàn)閾砣?duì)列的take方法支持響應(yīng)中斷,這樣直接關(guān)閉服務(wù)的方法就是強(qiáng)行關(guān)閉,強(qiáng)行關(guān)閉的方式不會(huì)去處理已經(jīng)提交但還未開始執(zhí)行的任務(wù)。

但是,關(guān)閉日志服務(wù)前,擁塞隊(duì)列中可能還有沒有及時(shí)打印出來的日志消息,所以強(qiáng)行關(guān)閉日志服務(wù)并不合適,需要等隊(duì)列中已經(jīng)存在的消息都打印完畢之后再停止,這就是平緩關(guān)閉,也就是在關(guān)閉服務(wù)時(shí)會(huì)等待已提交任務(wù)全部執(zhí)行完畢之后再退出。

除此之外,在取消生產(chǎn)者-消費(fèi)者操作時(shí),還需要同時(shí)告知消費(fèi)者和生產(chǎn)者相關(guān)操作已經(jīng)被取消。

平緩關(guān)閉的日志服務(wù)如下,其采用了類似信號(hào)量的方式記錄隊(duì)列中尚未處理的消息數(shù)量。

public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    @GuardedBy("this") private boolean isShutdown;
    // 信號(hào)量 用來記錄隊(duì)列中消息的個(gè)數(shù)
    @GuardedBy("this") private int reservations;

    public LogService(Writer writer) {
        this.queue = new LinkedBlockingQueue<String>();
        this.loggerThread = new LoggerThread();
        this.writer = new PrintWriter(writer);
    }

    public void start() {
        loggerThread.start();
    }

    public void stop() {
        synchronized (this) {
            isShutdown = true;
        }
        loggerThread.interrupt();
    }

    public void log(String msg) throws InterruptedException {
        synchronized (this) {
            //同步方法判斷是否關(guān)閉和修改信息量
            if (isShutdown) // 如果已關(guān)閉,則不再允許生產(chǎn)者將消息添加到隊(duì)列,會(huì)拋出異常
                throw new IllegalStateException(/*...*/);
            //如果在工作狀態(tài),信號(hào)量增加
            ++reservations;
        }
        // 消息入隊(duì)列;
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        public void run() {
            try {
                while (true) {
                    try {
                        //同步方法讀取關(guān)閉狀態(tài)和信息量
                        synchronized (LogService.this) {
                            //如果進(jìn)程被關(guān)閉且隊(duì)列中已經(jīng)沒有消息了,則消費(fèi)者退出
                            if (isShutdown && reservations == 0)
                                break;
                        }
                        // 取出消息
                        String msg = queue.take();
                        // 消費(fèi)消息前,修改信號(hào)量
                        synchronized (LogService.this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) { /* retry */
                    }
                }
            } finally {
                writer.close();
            }
        }
    }
}

2.2 關(guān)閉ExecutorService

ExecutorService中,其提供了shutdownshutdownNow方法來分別實(shí)現(xiàn)平緩關(guān)閉和強(qiáng)制關(guān)閉:

  • shutdownNow:強(qiáng)制關(guān)閉,響應(yīng)速度快,但是會(huì)有風(fēng)險(xiǎn),因?yàn)橛腥蝿?wù)肯執(zhí)行到一半被終止;
  • shutdown:平緩關(guān)閉,響應(yīng)速度較慢,會(huì)等到全部已提交的任務(wù)執(zhí)行完畢之后再退出,更為安全。

這里還需要說明下shutdownNow方法的局限性,因?yàn)閺?qiáng)行關(guān)閉直接關(guān)閉線程,所以無法通過常規(guī)的方法獲得哪些任務(wù)還沒有被執(zhí)行。這就會(huì)導(dǎo)致我們無紡知道線程的工作狀態(tài),就需要服務(wù)自身去記錄任務(wù)狀態(tài)。如下為示例代碼:

public class TrackingExecutor extends AbstractExecutorService {
    private final ExecutorService exec;

    //被取消任務(wù)的隊(duì)列
    private final Set<Runnable> tasksCancelledAtShutdown =
            Collections.synchronizedSet(new HashSet<Runnable>());

    public TrackingExecutor(ExecutorService exec) {
        this.exec = exec;
    }

    public void shutdown() {
        exec.shutdown();
    }

    public List<Runnable> shutdownNow() {
        return exec.shutdownNow();
    }

    public boolean isShutdown() {
        return exec.isShutdown();
    }

    public boolean isTerminated() {
        return exec.isTerminated();
    }

    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        return exec.awaitTermination(timeout, unit);
    }

    public List<Runnable> getCancelledTasks() {
        if (!exec.isTerminated())
            throw new IllegalStateException(/*...*/);
        return new ArrayList<Runnable>(tasksCancelledAtShutdown);
    }

    public void execute(final Runnable runnable) {
        exec.execute(new Runnable() {
            public void run() {
                try {
                    runnable.run();
                } finally {
                    // 如果當(dāng)前任務(wù)被中斷且執(zhí)行器被關(guān)閉,則將該任務(wù)加入到容器中
                    if (isShutdown()
                            && Thread.currentThread().isInterrupted())
                        tasksCancelledAtShutdown.add(runnable);
                }
            }
        });
    }
}

3. 處理非正常線程終止

導(dǎo)致線程非正常終止的主要原因就是RuntimeException,其表示為不可修復(fù)的錯(cuò)誤。一旦子線程拋出異常,該異常并不會(huì)被父線程捕獲,而是會(huì)直接拋出到控制臺(tái)。所以要認(rèn)真處理線程中的異常,盡量設(shè)計(jì)完備的try-catch-finally代碼塊。

當(dāng)然,異常總是會(huì)發(fā)生的,為了處理能主動(dòng)解決未檢測(cè)異常問題,Thread.API提供了接口UncaughtExceptionHandler

public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

如果JVM發(fā)現(xiàn)一個(gè)線程因未捕獲異常而退出,就會(huì)把該異常交個(gè)Thread對(duì)象設(shè)置的UncaughtExceptionHandler來處理,如果Thread對(duì)象沒有設(shè)置任何異常處理器,那么默認(rèn)的行為就是上面提到的拋出到控制臺(tái),在System.err中輸出。

Thread對(duì)象通過setUncaughtExceptionHandler方法來設(shè)置UncaughtExceptionHandler,比如這樣:

public class WitchCaughtThread  
{  
    public static void main(String args[])  
    {  
        Thread thread = new Thread(new Task());  
        thread.setUncaughtExceptionHandler(new ExceptionHandler());  
        thread.start();  
    }  
}  
  
class ExceptionHandler implements UncaughtExceptionHandler  
{  
    @Override  
    public void uncaughtException(Thread t, Throwable e)  
    {  
        System.out.println("==Exception: "+e.getMessage());  
    }  
}  

同樣可以為所有的Thread設(shè)置一個(gè)默認(rèn)的UncaughtExceptionHandler,通過調(diào)用Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法,這是Thread的一個(gè)static方法。

下面是一個(gè)例子,即發(fā)生為捕獲異常時(shí)將異常寫入日志:

public class UEHLogger implements Thread.UncaughtExceptionHandler {

    // 將未知的錯(cuò)誤計(jì)入到日志中
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
    }
}

Executor框架中,需要將異常的捕獲封裝到Runnable或者Callable中并通過execute提交的任務(wù),才能將它拋出的異常交給UncaughtExceptionHandler,而通過submit提交的任務(wù),無論是拋出的未檢測(cè)異常還是已檢查異常,都將被認(rèn)為是任務(wù)返回狀態(tài)的一部分。如果一個(gè)由submit提交的任務(wù)由于拋出了異常而結(jié)束,那么這個(gè)異常將被Future.get封裝在ExecutionException中重新拋出。

public class ExecuteCaught  
{  
    public static void main(String[] args)  
    {  
        ExecutorService exec = Executors.newCachedThreadPool();  
        exec.execute(new ThreadPoolTask());  
        exec.shutdown();  
    }  
}  
  
class ThreadPoolTask implements Runnable  
{  
    @Override  
    public void run()  
    {  
        Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler());  
        System.out.println(3/2);  
        System.out.println(3/0);  
        System.out.println(3/1);  
    }  
}  

擴(kuò)展閱讀:

  1. 多線程安全性:每個(gè)人都在談,但是不是每個(gè)人都談地清
  2. 對(duì)象共享:Java并發(fā)環(huán)境中的煩心事
  3. 從Java內(nèi)存模型角度理解安全初始化
  4. 從任務(wù)到線程:Java結(jié)構(gòu)化并發(fā)應(yīng)用程序
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,676評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評(píng)論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,873評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,266評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,482評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,036評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,846評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,025評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,279評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評(píng)論 1 289
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,751評(píng)論 3 394
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,016評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容

  • 下面是我自己收集整理的Java線程相關(guān)的面試題,可以用它來好好準(zhǔn)備面試。 參考文檔:-《Java核心技術(shù) 卷一》-...
    阿呆變Geek閱讀 14,884評(píng)論 14 507
  • 第三章 Java內(nèi)存模型 3.1 Java內(nèi)存模型的基礎(chǔ) 通信在共享內(nèi)存的模型里,通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱...
    澤毛閱讀 4,374評(píng)論 2 22
  • 一、并發(fā) 進(jìn)程:每個(gè)進(jìn)程都擁有自己的一套變量 線程:線程之間共享數(shù)據(jù) 1.線程 Java中為多線程任務(wù)提供了很多的...
    SeanMa閱讀 2,509評(píng)論 0 11
  • 一.線程安全性 線程安全是建立在對(duì)于對(duì)象狀態(tài)訪問操作進(jìn)行管理,特別是對(duì)共享的與可變的狀態(tài)的訪問 解釋下上面的話: ...
    黃大大吃不胖閱讀 860評(píng)論 0 3
  • 最近發(fā)現(xiàn)李笑來老師的課程已經(jīng)講完了,而我才學(xué)習(xí)到成長(zhǎng)率,不過有意思的是,成長(zhǎng)率和之前的幾期課程內(nèi)容上明顯需要應(yīng)用前...
    秦家炎閱讀 225評(píng)論 7 2