java 并發編程精華一頁紙

1、線程安全與鎖

線程安全的本質,在于 存在了共享的可變狀態 status, 在多線程共同操作狀態變量時,當計算的正確性依賴于運行時相關的時序(比如i++,先取出i,再+1,然后賦值),就會出現競爭(競態)

無狀態對象永遠是線程安全的.

所以線程安全 三步驟:a、無共享狀態;b、共享狀態不可變;c、共享狀態同步

要做到狀態同步,就必須要通過鎖 or volatile;本章節先討論鎖,被鎖保護的代碼塊可以認為是一間屋子,只有唯一的鎖可以打開,即同時只能只有一個線程進入。其他進入的線程必須拿到這把鎖。這樣就保證了數據的安全性。

I、內部鎖 synchronized

三種用法:a、鎖住變量 (這個變量就是鎖);b、鎖住方法 (擁有這個方法的對象是鎖) ;c、鎖住靜態方法 (這個類的Class對象是鎖)

每個java對象都自帶隱式鎖(也稱為監視器鎖)

可重入 - 每個線程可以對同一個鎖多次獲取,比如 下面這個例子 a 獲取一次,b還要再獲取,獲取時計數\退出時減少(像不像早期的垃圾回收)

public synchronized a(){

b();

}

public synchronized b(){

}

II、顯式鎖 ReentrantLock

ReentrantLock + Condition 替換了原先的 synchronized + object

Condition 實現了 資源的 競爭與釋放 ; Lock 實現了 代碼的隔離

Lock lock = new ReentrantLock();

Waits = lock.newConidition();

Lock.lock();

Try{

Waits.await() – 等同于 原先的鎖wait

}finnaly{

Lock.unlock

}

顯示鎖有什么好處?

a、tryLock( xxx ) -- 可以設置超時,無法獲取的時候,這樣可以避免死鎖

b、lockInterruptibly(); -- 可以響應中斷的鎖

c、性能提升? -- java6 以后的版本已經沒有性能優勢了

d、讀寫分離

顯式鎖里面,有專門的 ReentrantReadWriteLock 讀寫分離鎖。好吧,這里又看到類似數據庫的設計思路了。

讀鎖可以多線程同時進入,是共享的。寫鎖只能單線程進入,是排他的

e、公平

初始化 ReentrantLock 時,可以指定是否是公平的。何解? 因為線程調度喚醒線程時,是隨機的,有可能先來的線程未獲取到,這就不公平了。指定公平性,可以按等待順序獲取鎖

有啥缺點?

每次都需要顯式手工finnaly 關閉

內部鎖 or 顯式鎖?

如何選擇兩者。jdk 推薦 默認使用 內部鎖,因為簡單、方便;如果有特殊需求的,如上顯示鎖描述的好處。

III、死鎖

a、出現死鎖的條件:

互斥 - 資源只能被一個線程占有

請求與保持 - 占住的不想放,想要的非要

不可剝奪 - 占住的不能搶走

循環等待 - 只要有人放手就解開了,就是沒人放手

b、如果預防死鎖?

排好隊 - 如果非要獲取多個鎖,那所有的獲取鎖方式都要 按照一定順序

中斷(搶奪) - 顯式鎖可以中斷某個鎖

超時(放棄) - 顯式鎖可以設置超時放棄鎖

c、如何檢測死鎖?

打印堆棧

win( Ctrl + Break ) linux (kill -3)

Jmap+jhat+jstatck

監視工具

JConsole | Visual VM

IV、使用鎖的建議

a、如何選擇內部鎖和顯式鎖,默認內部鎖,特殊情況比如定時、中斷使用顯式。(參見上文描述)

b、盡量減少同步的范圍、 不要在 方法上加鎖,而是在方法內部

c、多個鎖按順序獲取,可以把獲取的地方統一隔離,放在一起,通過對象hash值identityHashCode安排順序

2、線程安全與對象(非阻塞式實現)

同步是 保護 對象(數據) 的一種方式,除了用鎖這種同步方式,還有其他效率更高的方式。

I、volatile - 共享變量

系統會監視這個變量,對它的操作不會和其他內存操作重排序,也就是說對volatile 變量的操作當成是原子操作;所有的修改其他線程都可見,也就不需要加鎖同步了。

volatile int a;

對volatile的使用有一些限制,主要是自增類的操作不能使用(比如i++)、只能單獨使用、不能組合使用

II、CAS 與原子類型 -- CAS = 比較并設置CompareAndSet

實現思路很簡單,就是當修改的值和 現在內存的值比較,如果一致了,就修改生效;如果不一致就一直循環檢查,直到生效為止。

public final long getAndDecrement() {

while (true) {

long current = get();

long next = current - 1;

if (compareAndSet(current, next))

return current;

}

}

volatile和 CAS 都是CPU層面提供的底層技術,不是語言本身提供的技術,其中CAS的實現還需要調用 JNI 本地化實現。

volatile + CAS 構成了java的原子類型庫 java\util\concurrent\atomic

a、基本類型的原子包裝器 AtomicInteger | AtomicLong | AtomicBoolean

b、數組類型 AtomicIntegerArray ...

c、對象類型 AtomicReference (包裝一個類) | 域更新器 AtomicReferenceFieldUpdater(利用反射,更細粒度的操作)

-- java自身的cocurrent類庫也有使用原子類型的,比如ConcurrentLinkedQueue 就使用了 域更新器實現 具體鏈表 字段的更新

該如何選擇 鎖 or 原子類型+volatile?

一般需要用在控制變量、標記值等等應用場景應該選用 原子類型這些非阻塞的同步方式;如果有大段代碼復雜邏輯需要保護,則采用同步方式。

III、封閉線程Ad-hoc 與ThreadLocal

把數據封閉在線程內部,就不存在共享狀態的問題,也就是線程安全了,最典型的應用就是 ThreadLocal,網上對ThreadLocal很多理解都有誤區

a、每個線程綁定一個 ThreadLocal.ThreadLocalMap 對象,線程內部的局部變量。

b、存放對象 set(T) 實際上就是 把 ThreadLocal 對象作為key ,T 作為value put 到 線程的map對象中

c、獲取對象 get,實際上就是把 this自身的對象 作為key 從 線程map中取出數據

幾個關鍵問題

a、多個線程的 ThreadLocal 對象雖然是同一個,但Map 是線程自己的,所以在調用 set get 時,獲取的是自己Map存儲的 value,實現了數據封閉

b、設置的value 必須是新的對象或者基本類型,否則如果設置的 是同一個 對象的引用的話,取出來還是同一個引用,就達到不到隔離的效果。

c、如果需要 在所有線程共享一個初始數據,可以 繼承ThreadLocal,擴展實現 initialValue 方法,這樣所有線程都能看到初始化數據

private static ThreadLocal> threadLog = new ThreadLocal>(){

protected List initialValue(){

return new ArrayList();

}

};

總結:ThreadLocal本質上是一個 空間換時間的 實現,通過在多個線程中 存儲不同的拷貝,實現了線程安全

3、線程生命周期

I、五種狀態

新建 - 兩種方法創建多線程:繼承java.lang.Thread,覆蓋run方法 | 實現java.lang.Runnable接口,實現run方法

就緒 - Thread.start - 對應run 方法

運行 - 線程搶到CPU開始運行

阻塞 - 遇到 鎖、休眠、IO等情況線程處于阻塞態 (等待、休眠)

終止 - 正常結束,被中斷 Thread.interrrupt

II、改變狀態

a、sleep - 當前線程休眠

b、yield - 當前線程讓渡CPU給同級別的線程

c、wait - 阻塞等待其他資源的鎖

d、join - 傻傻等其他線程結束后再結束

sleep vs wait

sleep 只是線程休眠,并不會釋放占有的資源,wait會釋放所有的資源;wait 是資源上的方法,而休眠是線程方法

sleep vs yield

sleep 線程切入休眠狀態,等待一定時間才喚醒搶占CPU;yield 只是暫時讓渡CPU

在構造函數中啟動線程是危險的,因為可能構造函數還未構造完成,如果回調或者其他就會出現異常。避免在構造函數啟動線程。

III、線程優先級

調度器根據線程優先級作為參考,決定多長時間調度該線程 ;Thread類中定義了默認3個級別 1 MIN ,5 NORMAL ,10 MAX

4、線程間通訊

嚴格來說 join 方法也算是一種線程間通訊,一個線程等待另一個線程處理結束,才繼續。

線程間通訊,不像進程間通訊Socket、共享內存、系統級別信號量,線程間因為共享資源,所以通訊方式大都是通過共享的資源。

傳統方式

I、經典通訊方式 wait/notify/notifyAll

使用wait/notify的正確姿勢

線程A

synchronized(list){

while(list.size == 0)

list.wait();

}

線程B

synchronized(list){

list.add(xxx);

list.notifyAll();

}

a、wait 方法一般都要寫在synchronized 的循環里

b、synchronized 鎖住的對象和 wait的對象一般是同一個對象

c、使用notifyAll 而不是 notify

wait 有無參數? 有參數只是多了一個喚醒條件,喚醒后和wait一樣繼續搶占資源

notify 和 notifyAll ? notifyAll喚醒所有線程,只有一個線程搶到,notify只隨機喚醒一個線程

線程屏障

II、閉鎖 CountDownLatch 和 關卡 CyclicBarrier

a、從功能上看,兩者很類似,都是用作阻塞的線程控制。

b、從語義上看,略有區別,閉鎖等待的是事件、關卡等待的是線程,兩者都是在await等待;閉鎖通過 countDown 這個事件、觸發等待的線程,而關卡需要互相等待、等所有線程處理完成后、觸發等待的線程。

c、更大的區別則是:CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得阻塞的線程數量。isBroken方法用來知道阻塞的線程是否被中斷。

III、信號量 Semaphore

和操作系統的信號量概念一樣,允許一個資源被被操作使用的次數

Semaphore s = new Semaphore(100);

申請 s.acquire()

釋放 s.release()

以上三種使用方法,wait/閉鎖/信號量 某種程度上都屬于資源申請;而閉鎖,則是單純的線程等待控制。下面介紹另一種方式,數據交換.

IV、Exchanger 數據交換

只能兩個線程進行數據交換

Exchange exc = new Exchange();

A線程執行 IN = exc.exchange(OUT) ,此時A線程阻塞

B線程執行 OUT = exc.exchange(IN) ,此時A、線程解除阻塞,雙方數據發生了交換

5、線程池

線程池一般有兩種實現方式:一種是 任務池,各個線程循環去池里取任務;一種是 線程池,提交任務后,從池里取出空閑線程處理。

I、ThreadPoolExecutor

任務隊列BlockingQueue - 使用了阻塞隊列作為提交的任務,接受繼承 Runnable 的任務

線程池HashSet - 最少corePoolSize 最大 maximumPoolSize 空閑時間 keepAliveTime ;這三個參數決定 線程數目在 最小和最大之間彈性處理(當 > 最少,且 > 空閑時間,釋放一部分空閑線程)

線程工廠ThreadFactory - 提供創建線程的方法 (還是為了擴展性, 給應用更多的創建線程前后的處理工作),Executors框架提供了一些線程工廠的實現

工作線程Worker - 實現了Runnable ,綁定了一個線程(ThreadFactory 創建線程時,傳入),執行提交的任務外,還有beforeExecute afterExecute 的鉤子函數

隊列管理

ThreadPoolExecutor 的隊列管理是開放出來由外界提供 BlockingQueue的實現,具體參見下面 并發框架的 Executors 提供的一些默認隊列

飽和策略

當有限隊列充滿后(即無法把任務提交到 BlockingQueue中去),就要有飽和策略。ThreadPoolExecutor的飽和策略,可以通過調用setRejectedExecutionHandler來修改

中止(abort)

遺棄(discard)

遺棄最舊的(discard-oldest)

調用者運行(caller-runs)

II、Executors/ExecutorServie(Executor) 框架

a、Executor 框架 -- 其實就是一個 Command模式

接口非常簡單,把一個Runnable線程作為參數

void execute(Runnable command);

一般情況下,不需要自己定義一個線程,而是提交任務給Executor框架

b、ExecutorService 框架 -- Active Object 模式(主動對象模式)

調用線程 方法的調用 和被調用線程的 方法執行 進行解耦

被調用線程內部綁定一個 線程對象,即ThredPoolExecutor 的Worker,自己管理線程的狀態

獲取被調用線程的引用

ExecutorService service = Executors.newFixedThreadPool(NTHREADS);

提交

service.execute(task);

service.submit(task);

還可以管理executor的生命周期

service.shutdown();

c、隊列

使用無限隊列 LinkedBlockingQueue (對于ThreadPoolExecutor 來說這些隊列都是有限的,可以把指定容量的隊列給他,但Executors 默認的都是無限未指定容量的隊列)

newFixedThreadPool -- 定長的線程池

newSingleThreadExecutor -- 單線程化的 executor

使用同步移交隊列 SynchronousQueue (沒有內存的隊列,直接把隊列從 提交者 給 消費者,據說等待性能更好?少了數據結構的操作開銷?)

newCachedThreadPool -- 可緩存的線程池

周期線程池

newScheduledThreadPool - 定長線程池,支持定時和周期的任務執行

Timer和TimerTask 的改進版本

原先的Timer存在一些缺陷,Timer調度是基于絕對時間,對系統時間改變很敏感。(ScheduledThreadPoolExecutor 支持相對時間);所有timer任務在一個線程執行,如果耗時操作導致其他任務延遲,會出現一些不可預料的錯誤Timer中如果timetask執行失敗,整個timer的任務都失敗

// 定時器

public class Timer {

public static final int TIME_DEFAULT = 15 * 60;

protected ScheduledExecutorService service = Executors.newScheduledThreadPool(1);

protected int period;

protected Runnable runnable;

public Timer(Runnable runnable){

this(TIME_DEFAULT, runnable);

}

public Timer(int period, Runnable runnable){

this.period = period;

this.runnable = runnable;

}

public void start(){

service.scheduleAtFixedRate(runnable, 0, period, TimeUnit.SECONDS);

}

public void destroy(){

service.shutdown();

}

// 絕對定時器,從下一個粒度整點開始

public static class AbsoluteTimer extends Timer{

public AbsoluteTimer(Runnable runnable) {

super(runnable);

}

public AbsoluteTimer(int period, Runnable runnable) {

super(period, runnable);

}

@Override

public void start(){

service.scheduleAtFixedRate(runnable, Utils.getNextWholeTime(new Date(), period), period, TimeUnit.SECONDS);

}

}

}

d、線程工廠

DefaultThreadFactory

PrivilegedThreadFactory

III、任務管理

任務的執行,很容易操作,直接Thread 的start,或者提交到 任務執行框架 Executor執行

任務取消和停止,如何控制? 怎么知道任務執行的結果? 怎么隔離一個耗時任務 ?

取消的方法一:通過共享一個變量標志來進行任務的協調。- cancellation requested

中斷的方法二:通過interrupt 中斷方法,取消一個線程的執行

獲取返回值

通過 局部變量,或者返回函數 -- 不可行,因為主線程和子線程并沒有先后順序的約束,受限于CPU、IO等復雜因素,結果是未知的

輪詢 -- 可行,采用類似阻塞方式,不停的去檢查是否完成,但多做了很多無用功,占用了CPU

回調 -- 正解

Future/FutureTask + Callable - 增強版的任務執行

Callable 提供 Runnable的增強功能 可以支持返回值

非阻塞的模型:Promise,Future 和 Callback 典型架構,Future 是異步執行方式

下面是一個簡版的 任務執行框架,可以把任務提交給Executors框架,通過 Future 來

// 執行任務

static interface Function{

Out action() throws Exception ;

}

// 提交任務框架, 任何一次性執行的, 有可能延遲的都可以使用

private static Out submit(final Function in){

final ExecutorService exec = Executors.newSingleThreadExecutor();

Future future = exec.submit(new Callable(){

@Override

public Out call() throws Exception {

return in.action();

}

});

Out result = null;

try {

result = future.get(STREAM_TIMEOUT_PER_TIME, TimeUnit.SECONDS);

} catch (Exception e) {

log.error(String.format("Task Thread %d Get submit error : %s ", Thread.currentThread().getId(), e.getMessage()), e.getCause());

}finally{

exec.shutdown();

}

return result;

}

Fork/Join 框架?

IV、CompletionService:Executor 和 BlockingQueue的組合

整合了 Executor 和 BlockingQueue的功能。調用時,把Callable任務提交給他,同時像quue一樣取出take和poll方法,返回的是一個Future

CompletionService 和 直接調用ExecutorService 區別在于,如果ExecutorService 啟動多個任務,獲取返回值時,多個Future需要管理起來,現在CompletionService直接用BlockingQueque對Future進行管理。

V、其他

線程增加 UncaughtExceptionHandler 捕獲未知的線程異常,通知框架和應用

6、其他

JVM線程關閉 時,會有一個鉤子函數

Runtime.getRuntime().addShutdownHook(new Thread(){

public void run(){

// 在這里做一些清理的工作。

}

});

守護線程 - 精靈線程 (dameon)

后臺服務線程,和JVM線程同在

用戶線程和守護線程區別不大,用戶線程隨時可以退出,守護線程一直到虛擬機的生命周期。 沒有用戶線程,守護線程和JVM一起退出。

一個完整的實際使用案例 - 這個可以用 JDK7的 Fork/Join 改寫

// 下載調度線程

public class DownLoadTask {

public int MAX_THREAD ;

public int MAX_RECORD_PER_THREAD ;

public static final int DEFAULT_TIME_OUT = 120000;

private FileWriter writer;

private List list;

private ExecutorService downPool ;

private PriorityBlockingQueue container;

private CyclicBarrier oneTimes;

private AtomicInteger interrupt = new AtomicInteger(0);

private AtomicInteger timeout = new AtomicInteger(0);

private static Logger log = Logger.getLogger(DownLoadTask.class);

public DownLoadTask(List list, FileWriter writer){

this.list = list;

this.writer = writer;

}

public void setMAX_THREAD(int MAX_THREAD) {

this.MAX_THREAD = MAX_THREAD;

}

public void setMAX_RECORD_PER_THREAD(int MAX_RECORD_PER_THREAD) {

this.MAX_RECORD_PER_THREAD = MAX_RECORD_PER_THREAD;

}

public void run(){

int ONE_SORT_TOTAL_NUM = MAX_THREAD * MAX_RECORD_PER_THREAD;

downPool = Executors.newFixedThreadPool(MAX_THREAD);

container = new PriorityBlockingQueue(ONE_SORT_TOTAL_NUM);

int recordsSize = list.size();

int repeatCount = recordsSize / ONE_SORT_TOTAL_NUM;

int lastRecords = recordsSize % ONE_SORT_TOTAL_NUM;

int lastRecordsWorkerNum = (int) ceil((double)lastRecords / (double) MAX_RECORD_PER_THREAD);

int lastRecordsLastWorkNum = lastRecords % MAX_RECORD_PER_THREAD;

log.info("repeatCount size:"+ repeatCount);

log.info("lastRecords size:"+ lastRecords);

log.info("lastRecordsWorkerNum:"+ lastRecordsWorkerNum);

log.info("lastRecordsLastWorkNum:"+ lastRecordsLastWorkNum);

if(repeatCount==0){

oneTimes = new CyclicBarrier(lastRecordsWorkerNum + 1, new Combine());

}else if(repeatCount > 0){

oneTimes = new CyclicBarrier(MAX_THREAD + 1, new Combine());

}

int i = 0;

int count = 0;

int index = 0;

List listfile = new ArrayList();

for(int j=0;j

SortFileData sortfile = new SortFileData();

sortfile.setFilestruct(list.get(j));

sortfile.setIndex(j);

listfile.add(sortfile);

i++;

if(listfile.size()== MAX_RECORD_PER_THREAD){

downPool.submit(new Worker(index, listfile));

index++;

if(i % ONE_SORT_TOTAL_NUM == 0 ){

count++;

waitForComplete();

if(count == repeatCount && lastRecords != 0){

oneTimes = new CyclicBarrier(lastRecordsWorkerNum + 1, new Combine());

}

}

listfile = new ArrayList();

}

}

if (lastRecordsLastWorkNum>0){

downPool.submit(new Worker(index, listfile));

}

if(lastRecordsWorkerNum != 0){

waitForComplete();

}

log.info(String.format("result [interrup = %d, timeout = %d]", interrupt.get(), timeout.get()));

}

// 線程阻塞

public void waitForComplete(){

try {

oneTimes.await(DEFAULT_TIME_OUT, TimeUnit.MILLISECONDS);

} catch (InterruptedException e) {

log.error("Thread interrupted.", e.getCause());

interrupt.incrementAndGet();

} catch (BrokenBarrierException e) {

log.info("CyclicBarrier reset", e.getCause());

} catch (TimeoutException e) {

log.error("Thread timeout.", e.getCause());

timeout.incrementAndGet();

}

}

// 用于排序的查詢結果

static class SortFileData implements Comparable{

int index;

byte[] data;

private FileStruct filestruct;

public void setIndex(int index) {

this.index = index;

}

public void setData(byte[] data) {

this.data = data;

}

public FileStruct getFilestruct() {

return filestruct;

}

public int getIndex(){

return index;

}

public byte[] getData(){

return data;

}

public void setFilestruct(FileStruct filestruct) {

this.filestruct = filestruct;

}

@Override

public int compareTo(SortFileData o) {

SortFileData that = o;

if(this.getIndex() > that.getIndex()) return 1;

else if(this.getIndex() < that.getIndex()) return -1;

return 0;

}

}

//工作線程

class Worker implements Runnable{

private FileStruct struct;

private int index;

private List listfile;

private DsuQuery query;

Worker(int index, List listfile){

this.index = index;

this.listfile = listfile;

}

public void setQuery(DsuQuery query) {

this.query = query;

}

@Override

public void run() {

query = new DsuQuery();

for (SortFileData file:listfile) {

byte[] data = Decode.decode(query.queryOnce(file.getFilestruct()),file.getFilestruct());

//byte[] data =new byte[0];

file.setData(data);

container.offer(file);

}

waitForComplete();

}

}

//合并線程,取出數據,并合并寫入文件

class Combine implements Runnable{

@Override

public void run() {

log.info("container size is "+ container.size());

while(!container.isEmpty()){

SortFileData result = container.poll();

writer.write(result.data);

//log.info("start write size is "+ result.data.length);

}

oneTimes.reset();

}

}

public static void main(String[] args) throws IOException {

List list = new ArrayList();

for(int i = 0 ; i < 14; i++){

FileStruct f = new FileStruct();

f.fileName = "test";

f.start = i;

f.length = 1;

f.timestab=i;

list.add(f);

}

LocalFileWriter filewriter = new LocalFileWriter(1);

DownLoadTask down=new DownLoadTask(list,filewriter);

down.run();

filewriter.close();

}

}

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

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,726評論 18 399
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,799評論 18 139
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,477評論 1 15
  • 隨著工程建設的發展,混凝土結構在我國各項工程建設中得到廣泛的應用。混凝土結構施工主要特點是取材便捷,可以在多種建筑...
    工程寶閱讀 1,151評論 0 0
  • 關于計算屬性的使用 vue中模板中使用表達式是很方便的一種方式,如: 但是如何將太多的邏輯放入模板中處理,就會使得...
    skills閱讀 501評論 0 1