Java學習24:多線程

什么是進程?什么是線程
進程是一個應用程序(1個進程是一個軟件)。
線程是一個進程中的執(zhí)行場景/執(zhí)行單元旗唁。
一個進程可以啟動多個線程。

對于java程序來說痹束,當在DOS命令窗口中輸入:
java HelloWorld 回車之后检疫。
會先啟動JVM,而JVM就是一個進程祷嘶。
JVM再啟動一個主線程調用main方法屎媳。
同時再啟動一個垃圾回收線程負責看護,回收垃圾论巍。
最起碼烛谊,現在的java程序中至少有兩個線程并發(fā),一個是垃圾回收線程嘉汰,一個是執(zhí)行main方法的主線程丹禀。

進程和線程是什么關系?舉個例子
阿里巴巴:進程
馬云:阿里巴巴的一個線程
童文紅:阿里巴巴的一個線程

京東:進程
強東:京東的一個線程
妹妹:京東的一個線程

進程可以看做是現實生活當中的公司鞋怀。
線程可以看做是公司當中的某個員工双泪。

注意:
進程A和進程B的內存獨立不共享。
線程A和線程B呢密似?
在java語言中:線程A和線程B焙矛,堆內存和方法區(qū)內存共享。但是棧內存獨立残腌,一個線程一個棧薄扁。

假設啟動10個線程,會有10個椃侠郏空間,每個棧和每個棧之間脱盲,互不干擾邑滨,各自執(zhí)行各自的,這就是多線程并發(fā)钱反。

火車站可以看作是一個進程掖看。
火車站中的每一個售票窗口可以看做是一個線程匣距。
我在窗口1購票,你可以在窗口2購票哎壳,你不需要等我毅待,我也不需要等你。
所以多線程并發(fā)可以提高效率归榕。

java中之所以有多線程機制尸红,目的就是為了提高程序的處理效率。

使用了多線程機制之后刹泄,main方法結束外里,是不是有可能程序也不會結束。main方法結束只是主線程結束了特石,主椫鸦龋空了,其它的棧(線程)可能還在壓棧彈棧姆蘸。

一個線程一個棧.png

對于單核的CPU來說墩莫,真的可以做到真正的多線程并發(fā)嗎?

對于多核的CPU電腦來說逞敷,真正的多線程并發(fā)是沒問題的狂秦。
4核CPU表示同一個時間點上,可以真正的有4個進程并發(fā)執(zhí)行兰粉。

什么是真正的多線程并發(fā)故痊?
t1線程執(zhí)行t1的。
t2線程執(zhí)行t2的玖姑。
t1不會影響t2.t2也不會影響t1愕秫。這叫做真正的多線程并發(fā)。

單核的CPU表示只有一個大腦:
不能夠做到真正的多線程并發(fā)焰络,但是可以做到給人一種“多線程并發(fā)”的感覺戴甩。
對于單核的CPU來說,在某一個時間點上實際上只能處理一件事情闪彼,但是由于CPU的處理速度極快甜孤,多個線程之間頻繁切換執(zhí)行,給人類的感覺就是:多個事情同時在做N吠蟆=纱ā!描馅!

實現線程的方式

java支持多線程機制把夸。并且Java已經將多線程實現了,我們只需要繼承就行了铭污。

實現線程有兩種方式恋日,哪兩種方式呢膀篮?
第一種方式:編寫一個類,直接繼承java.lang.Thread岂膳,重寫run方法誓竿。

//定義線程類
public class MyThread extends Thread{
public void run(){

}

}
//創(chuàng)建線程對象
MyThread t = new MyThread();
//啟動線程
t.start();

注意:
亙古不變的道理:方法體當中的代碼永遠都是自上而下的順序依次逐行執(zhí)行的。

public class Text {
    public static void main(String[] args){
        //這里是main方法谈截,這里的代碼屬于主線程筷屡,在主棧中運行。
        //新建一個分支線程對象
        MyThread t = new MyThread();
        //啟動線程
        //t.run();//不會啟動線程傻盟,不會分配新的分支棧速蕊。(這種方式就是單線程。)
        //start()方法的作用是:啟動一個分支線程娘赴,在JVM中開辟一個新的椆嬲埽空間,這段代碼任務完成之后诽表,瞬間就結束了唉锌。
        //這段代碼的任務只是為了開啟一個新的棧空間竿奏,只要新的棸兰颍空間開出來,start()方法就結束了泛啸。線程就啟動成功了绿语。
        //啟動成功的線程會自動調用run方法,并且run方法在分支棧的棧底部(壓棧)候址。
        //run方法在分支棧的棧底部吕粹,main方法在主棧的棧底部。run和main是平級的岗仑。
        t.start();
        //這里的代碼還是運行在主線程中匹耕。
        for (int i = 0; i < 1000; i++) {
            System.out.println("主線程--->" + i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        //編寫程序,這段程序運行在分支線程中(分支棧)荠雕。
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支線程--->" + i);
        }
    }
}

線程的run.png
線程的start.png

第二種方式:編寫一個類稳其,實現java.lang.Runnable接口,實現run方法炸卑。

//定義一個可運行的類
public class MyRunnable implements Runnable{
public void run(){

}

}
//創(chuàng)建線程對象
Thread t = new Thread(new MyRunnable());
//啟動線程
t.start();

public class Text {
    public static void main(String[] args){
        //創(chuàng)建一個可運行的對象
        //MyRunnable r = new MyRunnable();
        //將可運行的對象封裝成一個線程對象
        //Thread t = new Thread(r);
        //合并代碼
        Thread t = new Thread(new MyRunnable());
        //啟動線程
        t.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("主線程--->" + i);
        }
    }
}

//這并不是一個線程類既鞠,是一個可運行的類。它還不是一個線程盖文。
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("分支線程--->" + i);
        }
    }
}


注意:第二種方式實現接口比較常用损趋,因為一個類實現了接口,它還可以去繼承其它的類,更靈活浑槽。

可以采用匿名內部類來實現run方法

public class Text {
    public static void main(String[] args){
        //創(chuàng)建一個線程對象,采用匿名內部類的方式返帕。
        //這是通過一個沒有名字的類桐玻,new出來的對象。
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t線程--->" + i);
                }
            }
        });

        for (int i = 0; i < 100; i++) {
            System.out.println("main線程--->" + i);
        }
    }
}

線程的生命周期

新建狀態(tài)
就緒狀態(tài)
運行狀態(tài)
阻塞狀態(tài)
死亡狀態(tài)

線程生命周期.png

獲取線程名字

獲取線程對象的名字:
String name = 線程對象.getName();

修改線程對象的名字:
線程對象.setName("線程名字");

當線程沒有設置名字的時候荆萤,默認的名字有什么規(guī)律镊靴?(了解一下)
Thread-0
Thread-1
Thread-2
......

獲取當前線程對象

static Thread currentThread()

Thread t = Thread.currentThread();
System.out.println(t.getName());
返回值t就是當前線程

t就是當前線程對象。當前線程是誰呢链韭?
出現在主方法中偏竟,那就是主線程是當前線程。
當t1線程執(zhí)行run方法敞峭,那么這個當前線程就是t1踊谋。
當t2線程執(zhí)行run方法,那么這個當前線程就是t2旋讹。

線程的sleep()方法

static void sleep(Long millis)
靜態(tài)方法
參數是毫秒
作用:讓當前線程進入休眠殖蚕,進入“阻塞狀態(tài)”,放棄占有CPU時間片沉迹,讓給其它線程使用睦疫。

Thread.sleep()方法,可以做到這種效果:
間隔特定時間鞭呕,去執(zhí)行一段特定的代碼蛤育,每隔多久執(zhí)行一次。

public class Text {
    public static void main(String[] args) {
        //讓當前線程進入休眠葫松,睡眠5秒
        //當前線程是主線程M吒狻!
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //5秒后執(zhí)行這里的代碼
        System.out.println("hello world!");
    }
}
public class Text {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---->" + i);

            //睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

面試題:

public class Text {
    public static void main(String[] args) {
        //創(chuàng)建線程對象
        Thread t = new MyThread();
        t.setName("t");
        t.start();

        //調用sleep方法
        try {
            //問題:這行代碼會讓線程t進入休眠狀態(tài)么进宝?不會
            //這行代碼的作用是:讓當前線程進入休眠刻坊,也就是main線程進入休眠。
            //這行代碼出現在main方法中党晋,main方法睡眠谭胚。
            //雖然是用t調用的,但是跟t沒關系未玻,sleep方法是靜態(tài)方法
            t.sleep(1000 * 5);//在執(zhí)行的時候還是會轉換成:Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5秒之后灾而,這里才會執(zhí)行
        System.out.println("hello world!");
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

怎么叫醒一個正在睡眠的線程扳剿?
注意:這個不是終止線程的執(zhí)行旁趟,是終止線程的睡眠。

public class Text {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();

        //希望五秒之后庇绽,t線程醒來(就是這5秒鐘锡搜,假設主線程的活兒干完了)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            //打印異常信息
            e.printStackTrace();
        }

        //終止t線程的睡眠(這種終斷睡眠的方式依靠了java的異常處理機制橙困。)
        t.interrupt();//干擾,一盆冷水過去耕餐!
    }
}

class MyRunnable implements Runnable{

    //重點:run()當中的異常不能throws凡傅,只能try catch
    //因為run()方法在父類中沒有拋出任何異常,子類不能比父類拋出更多的異常肠缔。
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            //睡眠一年
            Thread.sleep(1000L * 60L * 60L * 24L * 365L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //1年之后才會執(zhí)行這里
        System.out.println(Thread.currentThread().getName() + "---> end");
    }
}

終止線程

在java中如何強行終止一個線程的執(zhí)行
這種方式存在很大的缺點:容易丟失數據夏跷。因為這種方式是直接將線程殺死了,線程沒有保存的數據將會丟失明未。不建議使用槽华。

public class Text {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();

        //模擬5秒
        try {
            Thread.sleep(100 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5秒后強行終止t線程
        t.stop();//已過時(不建議使用)
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

怎么合理的終止一個線程的執(zhí)行,這種方式是很常用的趟妥。

public class Text {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        //模擬5秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //終止線程
        //你想要什么時候終止t的執(zhí)行猫态,把標記修改為false,就結束了煮纵。
        r.run = false;

    }
}

class MyRunnable implements Runnable{

    //打一個布爾標記
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                //return就結束了懂鸵,在結束之前還有什么每保存的。
                //在這里就可以保存了
                //save.....

                //終止當前線程
                return;
            }

        }
    }
}

線程調度(了解)

常見的線程調度模型有哪些行疏?

搶占式調度模型:
哪個線程的優(yōu)先級比較高匆光,搶到的CPU時間片的概率就高一些/多一些。
java種采用的就是搶占式調度模型酿联。

均分式調度模型:
平均分配CPU時間片终息。每個線程占有的CPU時間片時間長度一樣。
平均分配贞让,一切平等周崭。
有一些編程語言,線程調度模型采用的是這種方式喳张。

java中提供了哪些方法是和線程調度有關系的呢续镇?

實例方法:
void setPriority(int newPriority) 設置線程的優(yōu)先級
int getPriority() 獲取線程的優(yōu)先級
最低優(yōu)先級是1
默認優(yōu)先級是5
最高優(yōu)先級是10
優(yōu)先級比較高的獲取CPU時間片可能會多一些。(但也不完全是销部,大概率是多的摸航。)

靜態(tài)方法:
static void yield() 讓位方法
暫停當前正在執(zhí)行的線程對象,并執(zhí)行其他線程
yield()方法不是阻塞方法舅桩。讓當前線程讓位酱虎,讓給其它線程使用。
yield()方法的執(zhí)行會讓當前線程從“運行狀態(tài)”回到“就緒狀態(tài)”擂涛。
注意:在回到就緒之后读串,有可能還會再次搶到CPU時間片。

實例方法:
void join() 合并線程

class MyThread1 extends Thread {
    public void doSome(){
        MyThread2 t = new MyThread2();
        t.join;//當前線程進入阻塞,t線程執(zhí)行恢暖,直到t線程結束排监。當前線程才可以繼續(xù)執(zhí)行。
    }
}
class MyThread2 extends Thread{

}

關于線程優(yōu)先級:
最高優(yōu)先級:Thread.MAX_PRIORITY
最低優(yōu)先級:Thread.MIN_PRIORITY
默認優(yōu)先級:Thread.NORM_PRIORITY

優(yōu)先級較高的杰捂,只是搶到的CPU時間片相對多一些社露。
大概率方向偏向于優(yōu)先級較高的
處于運行狀態(tài)的時間多一些。

設置線程優(yōu)先級:
Thread.currentThread().setPriority(1);//設置優(yōu)先級為1
t.setPriority(10);//設置t線程的優(yōu)先級為10

關于線程讓位:
當前線程暫颓砟铮回到就緒狀態(tài),讓給其他線程附鸽。
靜態(tài)方法:Thread.yield();

關于線程合并:
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
t.join();//t合并到當前線程中脱拼,當前線程受阻塞,t線程執(zhí)行坷备,直到結束熄浓。

線程安全(重點)

關于多線程并發(fā)環(huán)境下,數據的安全問題省撑。
為什么這個是重點赌蔑?
以后在開發(fā)中,我們的項目都是運行在服務器當中竟秫,而且服務器已經將線程的定義娃惯,線程對象的創(chuàng)建,線程的啟動等肥败,都已經實現完了趾浅。這些代碼我們都不需要編寫。

最重要的是:你要知道馒稍,你編寫的程序需要放到一個多線程的環(huán)境下運行皿哨,你更需要關注的是這些數據在多線程并發(fā)的環(huán)境下是否是安全的。

什么時候數據在多線程并發(fā)的環(huán)境下會存在安全問題呢纽谒?
三個條件:
條件一:多線程并發(fā)证膨。
條件2:有共享數據。
條件3:共享數據有修改的行為鼓黔。
滿足以上三個條件以后央勒,就會存在線程安全問題。

怎么解決線程安全問題呢请祖?
當多線程并發(fā)的環(huán)境下订歪,有共享數據,并且這個數據還會被修改肆捕,此時就存在線程安全問題刷晋,怎么解決這個問題?
線程排隊執(zhí)行。(不能并發(fā))
用排隊執(zhí)行解決線程安全問題眼虱。
這種機制被稱為:線程同步機制喻奥。
專業(yè)術語叫做:線程同不,實際上就是線程不能并發(fā)了捏悬,線程必須排隊執(zhí)行撞蚕。

線程同步就是線程排隊了,線程排隊了就回犧牲一部分效率过牙,沒辦法甥厦,數據安全第一位,只有數據安全了寇钉,我們才可以談效率刀疙。數據不安全,沒有效率的事兒扫倡。

線程同不這塊谦秧,設計到這兩個專業(yè)術語:
異步編程模型:
線程t1和線程t2,各自執(zhí)行各自的撵溃,t1不管t2疚鲤,t2不管t1,誰也不用等誰缘挑,這種編程模型叫做:異步編程模型集歇。
其實就是:多線程并發(fā)(效率較高。)
異步就是并發(fā)卖哎。

同步編程模型:
線程t1和線程t2鬼悠,在線程t1執(zhí)行的時候,必須等待t2線程執(zhí)行結束亏娜,或者說在t2線程執(zhí)行的時候焕窝,必須等待t1線程執(zhí)行結束,兩個線程之間發(fā)生了等待關系维贺,這就是同步編程模型它掂。
效率較低。線程排隊執(zhí)行溯泣。
同步就是排隊虐秋。

不使用線程同步機制,多線程對同一個賬戶進行取款垃沦,出現線程安全問題客给。

public class Text {
    public static void main(String[] args) {
        // 創(chuàng)建賬戶對象(只創(chuàng)建1個)
        Account act = new Account("act-001", 10000);
        // 創(chuàng)建兩個線程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 設置name
        t1.setName("t1");
        t2.setName("t2");
        // 啟動線程取款
        t1.start();
        t2.start();
    }
}

class AccountThread extends Thread {

    // 兩個線程必須共享同一個賬戶對象。
    private Account act;

    // 通過構造方法傳遞過來賬戶對象
    public AccountThread(Account act) {
        this.act = act;
    }

    public void run(){
        // run方法的執(zhí)行表示取款操作肢簿。
        // 假設取款5000
        double money = 5000;
        // 取款
        // 多線程并發(fā)執(zhí)行這個方法靶剑。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "對"+act.getActno()+"取款"+money+"成功蜻拨,余額" + act.getBalance());
    }
}

/*
銀行賬戶
    不使用線程同步機制,多線程對同一個賬戶進行取款桩引,出現線程安全問題缎讼。
 */
class Account {
    // 賬號
    private String actno;
    // 余額
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        // t1和t2并發(fā)這個方法。坑匠。血崭。。(t1和t2是兩個棧厘灼。兩個棧操作堆中同一個對象夹纫。)
        // 取款之前的余額
        double before = this.getBalance(); // 10000
        // 取款之后的余額
        double after = before - money;

        // 在這里模擬一下網絡延遲,100%會出現問題
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 更新余額
        // 思考:t1執(zhí)行到這里了设凹,但還沒有來得及執(zhí)行這行代碼捷凄,t2線程進來withdraw方法了。此時一定出問題围来。
        this.setBalance(after);
    }
}

使用線程同步機制,解決線程安全問題贤壁。
線程同步機制的語法是:
synchronized(){
//線程同步代碼塊
}
synchronized后面小括號中傳的這個“數據”是相當關鍵的寸潦。
這個數據必須是多線程共享的數據瑟慈。才能達到多線程排隊。

()中寫什么
那要看你想讓哪些線程同步胀蛮。
假設t1、t2糯钙、t3粪狼、t4、t5任岸,有5個線程再榄,你只希望t1、t2享潜、t3排隊困鸥,t4、t5不需要排隊剑按。怎么辦疾就?
你一定要在()中寫一個t1 t2 t3 共享的對象。而這個對象對于t4 t5來說不是共享的艺蝴。

public class Text {
    public static void main(String[] args) {
        // 創(chuàng)建賬戶對象(只創(chuàng)建1個)
        Account act = new Account("act-001", 10000);
        // 創(chuàng)建兩個線程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 設置name
        t1.setName("t1");
        t2.setName("t2");
        // 啟動線程取款
        t1.start();
        t2.start();
    }
}

class AccountThread extends Thread {

    // 兩個線程必須共享同一個賬戶對象猬腰。
    private Account act;

    // 通過構造方法傳遞過來賬戶對象
    public AccountThread(Account act) {
        this.act = act;
    }

    public void run(){
        // run方法的執(zhí)行表示取款操作。
        // 假設取款5000
        double money = 5000;
        // 取款
        // 多線程并發(fā)執(zhí)行這個方法猜敢。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "對"+act.getActno()+"取款"+money+"成功姑荷,余額" + act.getBalance());
    }
}

/*
銀行賬戶
    使用線程同步機制盒延,解決安全問題。
 */
class Account {
    // 賬號
    private String actno;
    // 余額
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        //以下這幾行代碼必須是線程排隊的厢拭,不能并發(fā)兰英。
        //一個線程把這里的代碼全部執(zhí)行結束之后,另一個線程才能進來供鸠。

        /*
        這里的共享對象是:賬戶對象畦贸。
        賬戶對象是共享的,那么this就是賬戶對象吧@阄妗薄坏!
        小括號里不一定是this,這里只要是多線程共享的那個對象就行寨闹。
        */
        synchronized (this){
            // 取款之前的余額
            double before = this.getBalance(); // 10000
            // 取款之后的余額
            double after = before - money;

            // 在這里模擬一下網絡延遲胶坠,100%會出現問題
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 更新余額
            // 思考:t1執(zhí)行到這里了,但還沒有來得及執(zhí)行這行代碼繁堡,t2線程進來withdraw方法了沈善。此時一定出問題。
            this.setBalance(after);
        }
    }
}

對synchronized的理解

在java語言中任何一個對象都有“一把鎖”椭蹄,其實這把鎖就是標記闻牡。(只是把它叫做鎖。)
100個對象绳矩,100把鎖罩润。1個對象1把鎖。

假設t1和t2線程并發(fā)翼馆,開始執(zhí)行同步代碼塊代碼的時候割以,肯定有一個先有一個后。
假設t1先執(zhí)行了应媚,遇到了synchronized严沥,這個時候自動找“后面共享對象”的對象鎖,找到之后中姜,并占有這把鎖祝峻,然后執(zhí)行同步代碼塊中的程序,在程序執(zhí)行過程中一致都是占有這把鎖的扎筒。知道同步代碼塊結束莱找,這把鎖才會釋放。
假設t1已經占有這把鎖嗜桌,此時t2也遇到了synchronized關鍵字奥溺,也會去占有后面共享對象的這把鎖,結果這把鎖被t1占有骨宠,t2只能在同步代碼塊外面等待t1的結束浮定,直到t1把同步代碼塊執(zhí)行結束了相满,t1會歸還這把鎖,此時t2終于等到這把鎖桦卒,然后t2占有這把鎖之后立美,進入同步代碼塊執(zhí)行程序。

這樣就達到了線程排隊執(zhí)行方灾。
這里需要注意的是:這個共享對象一定要選好了建蹄。這個共享對象一定是你需要排隊執(zhí)行的這些線程對象所共享的。

進入鎖池找鎖.png

哪些變量有線程安全問題
Java中有三大變量
實例變量:在堆中裕偿。
靜態(tài)變量:在方法區(qū)洞慎。
局部變量:在棧中。
以上三大變量中:
局部變量永遠都不會存在線程安全問題嘿棘。因為局部變量不共享劲腿。(一個線程一個棧。)
局部變量在棧中鸟妙,所以局部變量永遠都不會共享焦人。
實例變量在堆中,堆只有一個重父。
靜態(tài)變量在方法區(qū)中垃瞧,方法區(qū)只有1個。
堆和方法區(qū)都是多線程共享的坪郭,所以可能存在線程安全問題。

擴大同步范圍:
在調用方法的時候脉幢,我用個synchronized歪沃,也行,只不過是擴大了同步范圍嫌松,效率更低了沪曙。

synchronized出現在實例方法上,一定鎖的是this萎羔。沒得挑液走,只能是this。不能是其它對象了贾陷。所以這種方式不靈活缘眶。
另外還有一個缺點:synchronized出現在實例方法上表示整個方法體都需要同步,可能會無故擴大同步的范圍髓废,導致程序的執(zhí)行效率降低巷懈。所以這種方式不常用。

synchronized使用在實例方法上有什么優(yōu)點慌洪?
代碼寫的少了顶燕。節(jié)儉了凑保。
如果共享的對象就是this,并且需要同步的代碼塊是整個方法體涌攻,建議使用這種方式欧引。

如果使用局部變量的話:
建議使用:StringBuilder。
因為局部變量不存在線程安全問題恳谎。選擇StringBuilder芝此。StringBuffer效率比較低。

ArrayList是非線程安全的惠爽。
Vector是線程安全的癌蓖。
HashMap HashSet是非線程安全的。
Hashtable是線程安全的婚肆。

synchronized的三種寫法

第一種:同步代碼塊
靈活
synchronized(線程共享對象){
同步代碼塊;
}

第二種:在實例方法上使用synchronized
表示共享對象一定是this
并且同步代碼塊是整個方法體租副。

第三種:在靜態(tài)方法上使用synchronized
表示找類鎖。
類鎖永遠只有1把较性。
就算創(chuàng)建了100個對象用僧,那類鎖也只有一把。

對象鎖:1個對象一把鎖赞咙,100個對象100把鎖责循。
類鎖:100個對象,也可能只是1把類鎖攀操。

面試題

//面試題:doOther方法執(zhí)行的時候需要等待doSome方法的結束么院仿?
//不需要。因為doOther()方法沒有synchronized
public class MianShiTi1 {
    public static void main(String[] args) {
        MyClass mc = new MyClass();

        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);//這個睡眠的作用是:為了保證t1線程先執(zhí)行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass{
    //synchronized出現在實例方法上速和,表示鎖this歹垫。
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
//面試題:doOther方法執(zhí)行的時候需要等待doSome方法的結束么?
//需要颠放。
public class MianShiTi1 {
    public static void main(String[] args) {
        MyClass mc = new MyClass();

        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);//這個睡眠的作用是:為了保證t1線程先執(zhí)行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass{
    //synchronized出現在實例方法上排惨,表示鎖this。
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
//面試題:doOther方法執(zhí)行的時候需要等待doSome方法的結束么碰凶?
//不需要暮芭。因為MyClass的對象是兩個,兩把鎖
public class MianShiTi1 {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();

        Thread t1 = new MyThread(mc1);
        Thread t2 = new MyThread(mc2);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);//這個睡眠的作用是:為了保證t1線程先執(zhí)行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass{
    //synchronized出現在實例方法上欲低,表示鎖this辕宏。
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
//面試題:doOther方法執(zhí)行的時候需要等待doSome方法的結束么?
//需要砾莱。因為靜態(tài)方法是類鎖匾效,不管創(chuàng)建了幾個對象,類鎖只有一把
public class MianShiTi1 {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();

        Thread t1 = new MyThread(mc1);
        Thread t2 = new MyThread(mc2);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);//這個睡眠的作用是:為了保證t1線程先執(zhí)行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass{
    //synchronized出現在靜態(tài)方法上恤磷,表示類鎖面哼。
    public synchronized static void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

死鎖

/*
死鎖代碼要會寫野宜。
一般面試官要求你會寫。
只有會寫的魔策,才會在以后的開發(fā)中注意這個事兒匈子。
因為死鎖很難調試
*/

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        //t1和t2兩個線程共享o1.o2
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

        t1.start();
        t2.start();


    }
}

class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread{
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

synchronized在開發(fā)中最好不要嵌套使用。一不小心就可能會導致死鎖現象的發(fā)生闯袒。

開發(fā)中應該怎么解決線程安全問題虎敦?

難道一上來就選擇線程同步嗎?synchronized
不是政敢,synchronized 會讓程序的執(zhí)行效率降低其徙,用戶體驗不好。系統(tǒng)的用戶吞吐量降低喷户。用戶體驗差唾那。在不得已的情況下再選擇線程同步機制。

第一種方案:盡量使用局部變量代替“實例變量和靜態(tài)變量”褪尝。

第二種方案:如果必須是實例變量闹获,那么可以考慮創(chuàng)建多個對象,這樣實例變量的內存就不共享了河哑。(一個線程對應一個對象避诽,100個線程對應100個對象。對象不共享璃谨,就沒有數據安全問題了沙庐。)

第三種方案:如果不能使用局部變量,對象也不能創(chuàng)建多個佳吞,這個時候就只能選擇synchronized 了拱雏。線程同步機制。

守護線程

java語言中線程分為兩大類:
一類是:用戶線程
一類是:守護線程
其中具有代表性的就是:垃圾回收線程(守護線程)容达。

守護線程的特點:
一般守護線程是一個死循環(huán),所有的用戶線程只要結束垂券,守護線程自動結束花盐。
注意:主線程main方法是一個用戶線程。

守護線程用在什么地方呢菇爪?
每天00:00的時候系統(tǒng)數據自動備份算芯。
這個需要使用到定時器,并且我們可以將定時器設置為守護線程凳宙。一直在那里看著熙揍,每到00:00的時候就備份一次。所有的用戶線程如果結束了氏涩,守護線程自動退出届囚,沒有必要進行數據備份了有梆。

將線程設置為守護線程
t.setDaemon(true);

public class ShouHu {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("備份數據的線程");
        //啟動線程之前,將線程設置為守護線程
        t.setDaemon(true);
        t.start();

        //主線程:主線程是用戶線程
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread{
    public void run(){
        int i = 0;
        //即使是死循環(huán)意系,但由于該線程是守護者泥耀,當用戶進程結束,守護線程自動終止蛔添。
        while (true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定時器

定時器的作用:
間隔特定的時間痰催,執(zhí)行特定的程序。
每周要進行銀行賬戶的總賬操作迎瞧。
每天要進行數據的備份操作夸溶。

在實際的開發(fā)中,每隔多久執(zhí)行一段特定的程序凶硅,這種需求是很常見的缝裁,那么在java中其實可以采用多種方式實現:
可以使用sleep方法,睡眠咏尝,設置睡眠時間压语,每到這個時間點醒來,執(zhí)行任務编检。這種方式是最原始的定時器胎食。(比較low)

在java的類庫中已經寫好了一個定時器:java.util.Timer,可以直接拿來用允懂。
不過厕怜,這種方式在目前的開發(fā)中也很少用,因為現在有很多高級框架都是支持定時任務的蕾总。

在實際的開發(fā)中粥航,目前使用較多的是Spring框架中提供的SpringTask框架,這個框架只要進行簡單的配置生百,就可以完成定時器的任務递雀。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class DingShi {
    public static void main(String[] args) throws Exception{

        //創(chuàng)建定時器對象
        Timer timer = new Timer();
        //Timer timer = new Timer(true);//守護線程的方式

        //指定定時任務
        //timer.schedule(定時任務,第一次執(zhí)行時間蚀浆,間隔多久執(zhí)行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firsteTime = sdf.parse("2020-11-15 10:00:00");
        timer.schedule(new logTimerTask(),firsteTime,1000 * 10);
        //每年執(zhí)行一次
        //timer.schedule(new logTimerTask(),firsteTime,1000L * 60L * 60L * 24L * 365L);

        //匿名內部類方式也是可以的
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //code....
            }
        },firsteTime,1000 * 10);
    }
}

//編寫一個定時任務類
//假設這是一個記錄日志的定時任務
class logTimerTask extends TimerTask{

    @Override
    public void run() {
        //編寫你需要執(zhí)行的任務就行了
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":完成了一次數據備份缀程!");
    }
}

實現線程的第三種方式

實現Callable接口。
這種方式實現的線程可以獲取線程的返回值市俊。
之前學的那兩種方式是無法獲取 線程的返回值的杨凑,因為run方法返回void。

系統(tǒng)委派一個線程去執(zhí)行一個任務摆昧,該線程執(zhí)行完任務之后撩满,可能會有一個執(zhí)行結果,我們怎么能拿到這個執(zhí)行結果呢?
使用第三種方式:實現Callable接口方式伺帘。
這種方式的優(yōu)點:可以獲取到線程的執(zhí)行結果昭躺。
這種方式的缺點:效率比較低,在獲取t線程執(zhí)行結果的時候曼追,當前線程受阻塞窍仰,效率較低。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;//JUC包下的礼殊,屬于java的并發(fā)包驹吮,老JDK中沒有這個包。新特性晶伦。

public class Text {
    public static void main(String[] args) throws Exception{

        //第一步:創(chuàng)建一個“未來任務類”對象
        //參數非常重要碟狞,需要給一個Callable接口實現對象
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {//call()方法就相當于run方法。只不過這個有返回值
                //線程執(zhí)行一個任務婚陪,執(zhí)行之后可能會有一個執(zhí)行結果
                //模擬執(zhí)行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end");
                int a = 100;
                int b = 200;
                return a + b;//自動裝箱(300結果變成Integer)
            }
        });

        //創(chuàng)建線程對象
        Thread t = new Thread(task);

        //啟動線程
        t.start();

        //這里是main方法族沃,這是在主線程中。
        //在主線程中泌参,怎么獲取t線程的返回結果脆淹?
        //get()方法執(zhí)行會導致“當前線程阻塞”
        Object obj = task.get();
        System.out.println("線程執(zhí)行結果" + obj);

        //main方法這里的程序要想執(zhí)行必須等待get()方法的結束
        //而get()方法可能需要很久。因為get()方法是為了拿另一個線程的執(zhí)行結果
        //另一個線程執(zhí)行是需要時間的
        System.out.println("hello world");
    }
}

關于Object類中的wait和notify方法

第一:wait和notify方法不是線程對象的方法沽一,是java中任何一個java對象都有的方法盖溺,因為這兩個方法是Object類中自帶的。
wait方法和notify方法不是通過線程對象調用的铣缠。

第二:wait方法作用烘嘱?
Object o = new Object();
o.wait();
表示:讓正在o對象上活動的線程進入等待狀態(tài),無期限等待蝗蛙,直到被喚醒為止蝇庭。
o.wait();方法的調用,會讓讓“當前線程”(正在o對象上活動的線程)進入等待狀態(tài)捡硅。

第三:notify方法作用哮内?
Object o = new Object();
o.notify();
表示:喚醒整在o對象上等待的線程。

還有一個notifyAll()方法:
這個方法是喚醒o對象上出于等待的所有線程壮韭。

wait和notify方法的理解.png

生產者和消費者模式

生產者和消費者模式.png

使用wait方法和notify方法實現“生產者和消費者模式”

什么是“生產者和消費者模式”北发?
生產線程負責生產,消費線程負責消費泰涂。
生產線程和消費線程要達到均衡鲫竞。
這是一種特殊的業(yè)務需求辐怕,在這種特殊的情況下需要使用wait方法和notify方法逼蒙。

wait和notify方法不是線程對象的方法,是最普通java對象都有的方法寄疏。

wait方法和notify方法建立在線程同步的基礎之上是牢。因為多線程要同時操作一個倉庫僵井。有線程安全問題。

wait方法作用:o.wait();讓正在o對象上活動的線程t進入等待狀態(tài)驳棱,并且釋放掉t線程之前占有的o對象的鎖批什。

notify方法作用:o.notify();讓正在o對象上等待的線程喚醒,只是通知社搅,不會釋放o對象上之前占有的鎖驻债。

···
/*
模擬這樣一個要求:
倉庫我們采用List集合。
List集合中假設只能存儲1個元素形葬。
1個元素就表示倉庫滿了合呐。
如果List集合中元素個數是0,就表示倉庫空了笙以。
保證List集合中永遠都是最多存儲1個元素淌实。

必須做到這種效果:生產1個,消費1個猖腕。

*/

public class ThreadTest16 {
public static void main(String[] args) {
// 創(chuàng)建1個倉庫對象拆祈,共享的。
List list = new ArrayList();
// 創(chuàng)建兩個線程對象
// 生產者線程
Thread t1 = new Thread(new Producer(list));
// 消費者線程
Thread t2 = new Thread(new Consumer(list));

    t1.setName("生產者線程");
    t2.setName("消費者線程");

    t1.start();
    t2.start();
}

}

// 生產線程
class Producer implements Runnable {
// 倉庫
private List list;

public Producer(List list) {
    this.list = list;
}
@Override
public void run() {
    // 一直生產(使用死循環(huán)來模擬一直生產)
    while(true){
        // 給倉庫對象list加鎖倘感。
        synchronized (list){
            if(list.size() > 0){ // 大于0放坏,說明倉庫中已經有1個元素了。
                try {
                    // 當前線程進入等待狀態(tài)侠仇,并且釋放Producer之前占有的list集合的鎖轻姿。
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 程序能夠執(zhí)行到這里說明倉庫是空的,可以生產
            Object obj = new Object();
            list.add(obj);
            System.out.println(Thread.currentThread().getName() + "--->" + obj);
            // 喚醒消費者進行消費
            list.notifyAll();
        }
    }
}

}

// 消費線程
class Consumer implements Runnable {
// 倉庫
private List list;

public Consumer(List list) {
    this.list = list;
}

@Override
public void run() {
    // 一直消費
    while(true){
        synchronized (list) {
            if(list.size() == 0){
                try {
                    // 倉庫已經空了逻炊。
                    // 消費者線程等待互亮,釋放掉list集合的鎖
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 程序能夠執(zhí)行到此處說明倉庫中有數據,進行消費余素。
            Object obj = list.remove(0);
            System.out.println(Thread.currentThread().getName() + "--->" + obj);
            // 喚醒生產者生產豹休。
            list.notifyAll();
        }
    }
}

}
···

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市桨吊,隨后出現的幾起案子威根,更是在濱河造成了極大的恐慌,老刑警劉巖视乐,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洛搀,死亡現場離奇詭異,居然都是意外死亡佑淀,警方通過查閱死者的電腦和手機留美,發(fā)現死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谎砾,你說我怎么就攤上這事逢倍。” “怎么了景图?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵较雕,是天一觀的道長。 經常有香客問我挚币,道長亮蒋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任妆毕,我火速辦了婚禮宛蚓,結果婚禮上,老公的妹妹穿的比我還像新娘设塔。我一直安慰自己凄吏,他們只是感情好,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布闰蛔。 她就那樣靜靜地躺著痕钢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪序六。 梳的紋絲不亂的頭發(fā)上任连,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音例诀,去河邊找鬼随抠。 笑死,一個胖子當著我的面吹牛繁涂,可吹牛的內容都是我干的拱她。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扔罪,長吁一口氣:“原來是場噩夢啊……” “哼秉沼!你這毒婦竟也來了?” 一聲冷哼從身側響起矿酵,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤唬复,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后全肮,有當地人在樹林里發(fā)現了一具尸體敞咧,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年辜腺,在試婚紗的時候發(fā)現自己被綠了休建。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柱锹。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丰包,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情壤巷,我是刑警寧澤邑彪,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站胧华,受9級特大地震影響寄症,放射性物質發(fā)生泄漏。R本人自食惡果不足惜矩动,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一有巧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悲没,春花似錦篮迎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至栈戳,卻和暖如春岂傲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背子檀。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工镊掖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人褂痰。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓亩进,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缩歪。 傳聞我的和親對象是個殘疾皇子镐侯,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內容