sponsored links

「乾貨」java基礎之多執行緒

大家好,我是小黑,一個在網際網路"苟且偷生"的農民工。點贊再看,養成習慣呀。

微信搜尋【小黑說Java】有我的所有文章。

前段時間公司面試招人,發現好多小夥伴雖然已經有兩三年的工作經驗,但是對於一些Java基礎的知識掌握的都不是很紮實,所以小黑決定開始跟大家分享一些Java基礎相關的內容。首先這一期我們從Java的多執行緒開始。

好了,接下來進入正題,先來看看什麼是程序和執行緒。

程序VS執行緒

程序是計算機作業系統中的一個執行緒集合,是系統資源排程的基本單位,正在執行的一個程式,比如QQ,微信,音樂播放器等,在一個程序中至少包含一個執行緒。

執行緒是計算機作業系統中能夠進行運算排程的最小單位。一條執行緒實際上就是一段單一順序執行的程式碼。比如我們音樂播放器中的字幕展示,和聲音的播放,就是兩個獨立執行的執行緒。

「乾貨」java基礎之多執行緒

瞭解完程序和執行緒的區別,我們再來看一下併發和並行的概念。

併發VS並行

當有多個執行緒在操作時,如果系統只有一個CPU,假設這個CPU只有一個核心,則它根本不可能真正同時進行一個以上的執行緒,它只能把CPU執行時間劃分成若干個時間段,再將時間段分配給各個執行緒執行,在一個時間段的執行緒程式碼執行時,其它執行緒處於掛起狀。這種方式我們稱之為併發(Concurrent)。

當系統有一個以上CPU或者一個CPU有多個核心時,則執行緒的操作有可能非併發。當一個CPU執行一個執行緒時,另一個CPU可以執行另一個執行緒,兩個執行緒互不搶佔CPU資源,可以同時進行,這種方式我們稱之為並行(Parallel)。

讀完上面這段話,是不是感覺好像懂了,又好像沒懂?啥併發?啥並行?馬什麼梅?什麼冬梅?

彆著急,小黑先給大家用個通俗的例子解釋一下併發和並行的區別,然後再看上面這段話,相信大家就都能夠理解了。

你吃飯吃到一半,電話來了,你一直把飯吃完之後再去接電話,這就說明你不支援併發也不支援並行;

你吃飯吃到一半,電話來了,你去電話,然後吃一口飯,接一句電話,吃一口飯,接一句電話,這就說明你支援併發;

你吃飯吃到一半,電話來了,你妹接電話,你在一直吃飯,你妹在接電話,這就叫並行。

總結一下,併發的關鍵,是看你有沒有處理多個任務的能力,不是同時處理;

並行的關鍵是看能不能同時處理多個任務,那要想處理多個任務,就要有“你妹”(另一個CPU或者核心)的存在(怎麼感覺好像在罵人)。

Java中的執行緒

在Java作為一門高階計算機語言,同樣也有程序和執行緒的概念。

我們用Main方法啟動一個Java程式,其實就是啟動了一個Java程序,在這個程序中至少包含2個執行緒,另一個是用來做垃圾回收的GC執行緒。

Java中通常透過Thread類來建立執行緒,接下來我們看看具體是如何來做的。

執行緒的建立方式

要想在Java程式碼中要想自定義一個執行緒,可以透過繼承Thread類,然後建立自定義個類的物件,呼叫該物件的start()方法來啟動。

public class ThreadDemo {
    public static void main(String[] args) {
        new MyThread().start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("這是我自定義的執行緒");
    }
}

或者實現java.lang.Runnable介面,在建立Thread類的物件時,將自定義java.lang.Runnable介面的例項物件作為引數傳給Thread,然後呼叫start()方法啟動。

public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(new MyRunnable()).s
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("這是我自定義的執行緒");
    }
}

那在實際開發過程中,是建立Thread的子類,還是實現Runnable介面呢?其實並沒有一個確定的答案,我個人更喜歡實現Runnable介面這種用法。在以後要學的執行緒池中也是對於Runnable介面的例項進行管理。當然我們也要根據實際場景靈活變通。

執行緒的啟動和停止

從上面的程式碼中我們其實已經看到,建立執行緒之後透過呼叫start()方法就可以實現執行緒的啟動。

new MyThread().start();

注意,我們看到從上一節的程式碼中看到我們自定義的Thread類是重寫了父類的run()方法,那我們直接呼叫run()方法可不可以啟動一個執行緒呢?答案是不可以。直接呼叫run()方法和普通的方法呼叫沒有區別,不會開啟一個新執行緒執行,這裡一定要注意。

那要怎麼來停止一個執行緒呢?我們看Thread類的方法,是有一個stop()方法的。

@Deprecated // 已經棄用了。
public final void stop() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if (this != Thread.currentThread()) {
            security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
    if (threadStatus != 0) {
        resume();
    }
    stop0(new ThreadDeath());
}

但是我們從這個方法上可以看到是加了@Deprecated註解的,也就是這個方法被JDK棄用了。被棄用的原因是因為透過stop()方法會強制讓這個執行緒停止,這對於執行緒中正在執行的程式是不安全的,就好比你正在拉屎,別人強制不讓你拉了,這個時候你是夾斷還是不夾斷(這個例子有點噁心,但是很形象哈哈)。所以在需要停止形成的是不不能使用stop方法。

那我們應該怎樣合理地讓一個執行緒停止呢,主要有以下2種方法:

第一種:使用標誌位終止執行緒

class MyRunnable implements Runnable {
    private volatile boolean exit = false; // volatile關鍵字,保證主執行緒修改後當前執行緒能夠看到被改後的值(可見性)
    @Override
    public void run() {
        while (!exit) { // 迴圈判斷標識位,是否需要退出
            System.out.println("這是我自定義的執行緒");
        }
    }
    public void setExit(boolean exit) {
        this.exit = exit;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
        runnable.setExit(true); //修改標誌位,退出執行緒
    }
}

線上程中定義一個標誌位,透過判斷標誌位的值決定是否繼續執行,在主執行緒中透過修改標誌位的值達到讓執行緒停止的目的。

第二種:使用interrupt()中斷執行緒

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        Thread.sleep(10);
        t.interrupt(); // 企圖讓執行緒中斷
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            System.out.println("執行緒正在執行~" + i);
        }
    }
}

這裡需要注意的點,就是interrupt()方法並不會像使用標誌位或者stop()方法一樣,讓執行緒馬上停止,如果你執行上面這段程式碼會發現,執行緒t並不會被中斷。那麼如何才能讓執行緒t停止呢?這個時候就要關注Thread類的另外兩個方法。

public static boolean interrupted(); // 判斷是否被中斷,並清除當前中斷狀態
private native boolean isInterrupted(boolean ClearInterrupted); // 判斷是否被中斷,透過ClearInterrupted決定是否清楚中斷狀態

那麼我們再來修改一下上面的程式碼。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        Thread.sleep(10);
        t.interrupt();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            //if (Thread.currentThread().isInterrupted()) {
            if (Thread.interrupted()) {
                break;
            }
            System.out.println("執行緒正在執行~" + i);
        }
    }
}

這個時候執行緒t就會被中斷執行。

到這裡大家其實會有個疑惑,這種方式和上面的透過標誌位的方式好像沒有什麼區別呀,都是判斷一個狀態,然後決定要不要結束執行,它們倆到底有啥區別呢?這裡其實就涉及到另一個東西叫做執行緒狀態,如果當執行緒t在sleep()或者wait()的時候,如果用標識位的方式,其實並不能立馬讓執行緒中斷,只能等sleep()結束或者wait()被喚醒之後才能中斷。但是用第二種方式,線上程休眠時,如果呼叫interrupt()方法,那麼就會丟擲一個異常InterruptedException,然後執行緒繼續執行。

執行緒的狀態

透過上面對於執行緒停止方法的對比,我們瞭解到執行緒除了執行和停止這兩種狀態意外,還有wait(),sleep()這樣的方法,可以讓執行緒進入到等待或者休眠的狀態,那麼執行緒具體都哪些狀態呢?其實透過程式碼我們能夠找到一些答案。在Thread類中有一個叫State的列舉類,這個列舉類中定義了執行緒的6中狀態。

public enum State {
    /**
     * 尚未啟動的執行緒的執行緒狀態
     */
    NEW,
    /**
     * 可執行狀態
     */
    RUNNABLE,
    /**
     * 阻塞狀態
     */
    BLOCKED,
    /**
     * 等待狀態
     */
    WAITING,
    /**
     * 超時等待狀態
     */
    TIMED_WAITING,
    /**
     * 終止狀態
     */
    TERMINATED;
}

那麼執行緒中的這六種狀態到底是怎麼變化的呢?什麼時候時RUNNABLE,什麼時候BLOCKED,我們透過下面的圖來展示執行緒見狀態發生變化的情況。

「乾貨」java基礎之多執行緒

執行緒狀態詳細說明

初始化狀態(NEW)

在一個Thread例項被new出來時,這個執行緒物件的狀態就是初始化(NEW)狀態。

可執行狀態(RUNNABLE)

  1. 在呼叫start()方法後,這個執行緒就到達可執行狀態,注意,可執行狀態並不代表一定在執行,因為作業系統的CPU資源要輪換執行(也就是最開始說的併發),要等作業系統排程,只有被排程到才會開始執行,所以這裡只是到達就緒(READY)狀態,說明有資格被系統排程;
  2. 當系統排程本執行緒之後,本執行緒會到達執行中(RUNNING)狀態,在這個狀態如果本執行緒獲取到的CPU時間片用完以後,或者呼叫yield()方法,會重新進入到就緒狀態,等待下一次被排程;
  3. 當某個休眠執行緒被notify(),會進入到就緒狀態;
  4. 被park(Thread)的執行緒又被unpark(Thread),會進入到就緒狀態;
  5. 超時等待的執行緒時間到時,會進入到就緒狀態;
  6. 同步程式碼塊或同步方法獲取到鎖資源時,會進入到就緒狀態;

超時等待(TIMED_WAITING)

當執行緒呼叫sleep(long),join(long)等方法,或者同步程式碼中鎖物件呼叫wait(long),以及LockSupport.arkNanos(long),LockSupport.parkUntil(long)這些方法都會讓執行緒進入超時等待狀態。

等待(WAITING)

等待狀態和超時等待狀態的區別主要是沒有指定等待多長的時間,像Thread.join(),鎖物件呼叫wait(),LockSupport.park()等這些方法會讓執行緒進入等待狀態。

阻塞(BLOCKED)

阻塞狀態主要發生在獲取某些資源時,在獲取成功之前,會進入阻塞狀態,知道獲取成功以後,才會進入可執行狀態中的就緒狀態。

終止(TERMINATED)

終止狀態很好理解,就是當前執行緒執行結束,這個時候就進入終止狀態。這個時候這個執行緒物件也許是存活的,但是沒有辦法讓它再去執行。所謂“執行緒”死不能復生。

執行緒重要的方法

從上一節我們看到執行緒狀態之間變化會有很多方法的呼叫,像Join(),yield(),wait(),notify(),notifyAll(),這麼多方法,具體都是什麼作用,我們來看一下。

上面我們講到過的start()、run()、interrupt()、isInterrupted()、interrupted()這些方法想必都已經理解了,這裡不做過多的贅述。

/**
 * sleep()方法是讓當前執行緒休眠若干時間,它會丟擲一個InterruptedException中斷異常。
 * 這個異常不是執行時異常,必須捕獲且處理,當執行緒在sleep()休眠時,如果被中斷,這個異常就會產生。
 * 一旦被中斷後,丟擲異常,會清除標記位,如果不加處理,下一次迴圈開始時,就無法捕獲這個中斷,故一般在異常處理時再設定標記位。
 * sleep()方法不會釋放任何物件的鎖資源。
 */
public static native void sleep(long millis) throws InterruptedException;

/**
 * yield()方法是個靜態方法,一旦執行,他會使當前執行緒讓出CPU。讓出CPU不代表當前執行緒不執行了,還會進行CPU資源的爭奪。
 * 如果一個執行緒不重要或優先順序比較低,可以用這個方法,把資源給重要的執行緒去做。
 */
public static native void yield();
/**
 * join()方法表示無限的等待,他會一直阻塞當前執行緒,只到目標執行緒執行完畢。
 */
public final void join() throws InterruptedException ;
/**
 * join(long millis) 給出了一個最大等待時間,如果超過給定的時間目標執行緒還在執行,當前執行緒就不等了,繼續往下執行。
 */
public final synchronized void join(long millis) throws InterruptedException ;

以上這些方法是Thread類中的方法,從方法簽名可以看出,sleep()和yield()方法是靜態方法,而join()方法是成員方法。

而wait(),notify(),notifyAll()這三個方式是Object類中的方法,這三個方法主要用於在同步方法或同步程式碼塊中,用於對共享資源有競爭的執行緒之間的通訊。

/**
 * 使當前執行緒等待,直到另一個執行緒呼叫該物件的 notify()方法或 notifyAll()方法。
 */
public final void wait() throws InterruptedException
/**
 * 喚醒正在等待物件監視器的單個執行緒。
 */
public final native void notify();
/**
 * 喚醒正在等待物件監視器的所有執行緒。
 */
public final native void notifyAll();

針對wait(),notify/notifyAll() 有一個典型的案例:生產者消費者,透過這個案例能加深大家對於這三個方法的印象。

場景如下:

假設現在有一個KFC(KFC給你多少錢,我金拱門出雙倍),裡面有漢堡在銷售,為了漢堡的新鮮呢,店員在製作時最多不會製作超過10個,然後會有顧客來購買漢堡。當漢堡數量到10個時,店員要停止製作,而當數量等於0也就是賣完了的時候,顧客得等新漢堡製作處理。

我們現在透過兩個執行緒一個來製作,一個來購買,來模擬這個場景。程式碼如下:

class KFC {
    // 漢堡數量
    int hamburgerNum = 0; 

    public void product() {
        synchronized (this) {
            while (hamburgerNum == 10) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("生產一個漢堡" + (++hamburgerNum));
            this.notifyAll();
        }
    }

    public void consumer() {
        synchronized (this) {
            while (hamburgerNum == 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("賣出一個漢堡" + (hamburgerNum--));
            this.notifyAll();
        }
    }
}
public class ProdConsDemo {
    public static void main(String[] args) {
        KFC kfc = new KFC();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                kfc.product();
            }
        }, "店員").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                kfc.consumer();
            }
        }, "顧客").start();

    }
}

從上面的程式碼可以看出,這三個方法是要配合使用的。

wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。

wait()使當前執行緒阻塞,前提是必須先獲得鎖,一般配合synchronized關鍵字使用。

當執行緒執行wait()方法時,會釋放當前的鎖,然後讓出CPU,進入等待狀態。

由於 wait()、notify/notifyAll() 在synchronized 程式碼塊執行,說明當前執行緒一定是獲取了鎖的。只有當notify/notifyAll()被執行時,才會喚醒一個或多個正處於等待狀態的執行緒,然後繼續往下執行,直到執行完synchronized程式碼塊的程式碼或是中途遇到wait() ,再次釋放鎖。

要注意,notify/notifyAll()喚醒沉睡的執行緒後,執行緒會接著上次的執行繼續往下執行。所以在進行條件判斷時候,不能使用if來判斷,假設存在多個顧客來購買,當被喚醒之後如果不做判斷直接去買,有可能已經被另一個顧客買完了,所以一定要用while判斷,在被喚醒之後重新進行一次判斷。

最後再強調一下wait()和我們上面講到的sleep()的區別,sleep()可以隨時隨地執行,不一定在同步程式碼塊中,所以在同步程式碼塊中呼叫也不會釋放鎖,而wait()方法的呼叫必須是在同步程式碼中,並且會釋放鎖。



好了,今天的內容就到這裡。我是小黑,我們下期見。

分類: 新聞
時間: 2021-09-18

相關文章

「乾貨」背景牆顏色搭配技巧,“牆”勢來襲

「乾貨」背景牆顏色搭配技巧,“牆”勢來襲
你對家裡的沙發背景牆.床頭背景牆設計還滿意嗎?背景牆的樣式確實不少,有大理石.護牆板.桌布.掛畫.裝飾框線等等,但是要想不出錯,配色才是最基礎的功課,配色選對才能"避免災難".要想 ...

「乾貨」藍寶石常見仿品及其鑑別特徵

「乾貨」藍寶石常見仿品及其鑑別特徵
在開始今天的內容之前,我們先來做個題,請問以下哪顆寶石是藍寶石? 你答對了嗎? 可見,可以作為藍寶石仿品的寶石材料可不少.在日常購買過程中,如果僅憑藉外觀,我們很容易將其他藍色系寶石錯認成藍寶石.我們 ...

「乾貨」“心機”擺件,點亮家的每個角落

「乾貨」“心機”擺件,點亮家的每個角落
身處一成不變的居所,時而也需要增添一些新鮮趣味.透過增加新奇有趣的物品,變動傢俱位置,來給生活注入活力.小變動,也可以成為畫龍點睛的一筆. #1 藝術家居掛畫 看似自由隨意的線條和形狀,展露出孩童般的 ...

「乾貨」新手化妝刷介紹❗️建議收藏❗️❗️
想要精緻的妝容一套好的化妝刷必不可少! 你們是不是還在用手化妝 這樣不仔細只會讓妝容更顯髒! 今天就來給大家講解一下每根刷子的用法吧~ ️散粉刷 散粉刷是整套化妝刷中最大的一支,適用於上散粉.粉餅等, ...

堅持「早起」有多了不起?身體5個變化贏在起跑線

堅持「早起」有多了不起?身體5個變化贏在起跑線
有人說,那些真正厲害的人,從不熬夜通宵拼命,而是習慣用「早起」的方式開啟新的一天. 中國晚清時期政治家.戰略家曾國藩 一輩子堅持早起的習慣 並告誡子孫後代一定要早起 ▼ 中國現當代著名文學家梁實秋 黎 ...

為什麼我們不喜歡「周冬雨」排列?

為什麼我們不喜歡「周冬雨」排列?
當然,我們說的並不是周冬雨本人,而是被稱為「周冬雨」排列的螢幕. 這個問題不止數碼愛好者感興趣,就連女演員周冬雨本人,也在知乎問出了這樣的問題:周冬雨排列是什麼? 隨著 OPPO 釋出屏下攝像頭的解決 ...

首席評測官·我樂家居「之間」系列 尋找這個世界從未有過的美學

首席評測官·我樂家居「之間」系列 尋找這個世界從未有過的美學
首席測評官:Allen Chou 設計師Allen Chou,畢業自英國皇家美術學院,從事室內設計工作十五年,專注於高階家居設計,別墅.大平層等豪宅優秀設計案例100+,服務金領菁英使用者200+. ...

數字時代的「 局外人 」

數字時代的「 局外人 」
圖源:抖音公益微電影<局外人> " 隨著<中國網際網路絡發展狀況統計報告>顯示:60歲及以上的非網民約1.91億,約佔73.4%.父母輩成為了數字生活的局外人. 不會 ...

回頭率 90% 以上的球鞋,全靠這些「細節」|《每週冷門球鞋大賞》

回頭率 90% 以上的球鞋,全靠這些「細節」|《每週冷門球鞋大賞》
和大家聊了這麼多期球鞋,從皮質聊到產地,從配色聊到科技,那都給阿正說完了可咋整?哈哈,放心~主題我來想,大家等著"上菜"就行!那今天的重點呢,我們就放在「回頭率」上,不知道對於大家 ...

專業評測:A.O.史密斯「瀞」油煙機 廚房消音新神器

專業評測:A.O.史密斯「瀞」油煙機 廚房消音新神器
作為一個"自助"美食家,小編在疫情期間廚藝大漲,讓自己和家人大飽口福!其實真正讓我愛上做飯的,除了那欲罷不能的一點點小天賦,更是得益於我家廚房最得力的小幫手--A.O.史密斯頂側雙 ...

日本品牌「IPSA」、「SOU・SOU」該怎麼念?正確的念法應該是這樣

日本品牌「IPSA」、「SOU・SOU」該怎麼念?正確的念法應該是這樣
日本化妝品牌「IPSA」.設計雜貨「SOU・SOU」.還有服飾品牌「COEN」,這些牌子你都怎麼念?是否常講到嘴邊就卡住,不知道該念字母還是拼音呢?「樂吃購!日本」整理了5個在中國常見的日本品牌正確讀 ...

老靈魂新技藝:Audemars Piguet全新「Re」master01計時腕錶
[Re]master01裡住著一個來自1943年的靈魂,若按照人類的年齡定義,也已到了從心所欲的年紀.它的確有著一張歷經歲月的面容,若你正巧看過AP歷史資料庫中的Ref. 1533計時碼錶,會發現它們 ...

SELAH愢拉2022春夏釋出會 以「芭蕾」之美詮釋愛

SELAH愢拉2022春夏釋出會 以「芭蕾」之美詮釋愛
蹁躚起舞 像露水一般輕盈 每一次跳躍 都能聽見天使的嘆息 讓她跳完她的舞 現實太狹窄了 讓她在「芭蕾」中做完塵世的夢 愛之於女性 亦如芭蕾 如覺醒,如盛放 如奔赴自由的輕盈一躍 相信愛情的人, Bel ...

一文讀懂子不語IPO:跨境電商「黑馬」年利潤過億

一文讀懂子不語IPO:跨境電商「黑馬」年利潤過億
"截至2020年底,子不語自主設計品牌數量已達151個,自營網站收入佔比大增." 本文為IPO早知道原創 作者|蘇打 疫情的持續蔓延,在衝擊許多行業發展軌跡的同時,也顛覆著線上的消 ...

二手值得買 | 小米 10 Pro:官網下架後反而更火的「真」旗艦

二手值得買 | 小米 10 Pro:官網下架後反而更火的「真」旗艦
或許這是小米數字系列歷史上最短命的旗艦了. 2020 年恰好是小米公司的 10 週年,年中推出了紀念版的小米 10 Ultra 不久後,小米官方就悄悄地讓它提前退市,讓位給小米 10 Ultra,作小 ...

「翡翠」創匯期飄陽綠“連年有餘”翡翠圓佩

「翡翠」創匯期飄陽綠“連年有餘”翡翠圓佩
我是@田地裡的甲殼蟲!超級喜歡翡翠,深深陷入其中 ,不能自拔!藉助"今日頭條"這個平臺,和同好們學習,分享翡翠的知識和美圖! 讀書筆記 連年有餘是由蓮花和鯉魚組成的中國傳統的吉祥圖 ...

「查德」走進“查德湖”上的島嶼村莊

「查德」走進“查德湖”上的島嶼村莊
我們一行四人乘著吉普車,用了兩天半時間穿越喀麥隆北部,於2014年1月6日下午,到達了查德的首都恩賈梅納. 喀麥隆和查德的陸路海關就在恩賈梅納近郊,幸好我們司機的弟弟是這個關口的工作人員,我們沒有下車 ...

36氪獨家丨戶外家居品牌「Outer」完成5000萬美元B輪融資,估值較上一輪漲近10倍

36氪獨家丨戶外家居品牌「Outer」完成5000萬美元B輪融資,估值較上一輪漲近10倍
36氪獲悉,戶外家居品牌「Outer」近期已完成5000萬美元B輪融資,本輪融資由今日資本領投,Tribe Capital.C資本.Upfront Ventures以及老股東紅杉資本中國基金.Muck ...

糟滷拼盤 | 糟了!整個冰箱都不夠我拿來「滷」

糟滷拼盤 | 糟了!整個冰箱都不夠我拿來「滷」
夏將盡,秋已至.一晃神,今日就已經立秋了. 聽起來雖說是秋季的頭一個節氣,但暑氣還是不講道理賴著不散.何以解暑熱?唯有下酒菜! 對北方人民來說,「糟貨」可能有些陌生.不過對於包郵區來說,糟貨可是夏季必 ...