淺談Java多線程

Java多線程的概念及創建方法

一、首先我們需要明白幾個概念:程序、進程和線程

程序:指令集,是一個靜態概念,比如說桌面上的一個應用就是一個程序。不管它運不運行都是一個程序

進程:操作系統調度程序,是一個動態概念。還是拿上面那個例子,當我們點擊運行的時候,操作系統開始調度,這就啓動了一個進程。

線程:線程是程序運行 的最小單位。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。(百度上copy過來的,哈哈),它和進程的關係是在一個進程內可以有多個線程,實際上它就是程序運行之後,進程執行的多條路徑。多線程就是指一個進程可以有多條執行路徑。比如說:一個人在桌子上吃飯,這是單進程單線程。多個人在一桌吃飯,這是單進程多線程。簡而言之就是,有多個人同時使用同一個資源,這就是多線程。多線程涉及到的主要有併發、死鎖、線程安全等問題。接下來就來具體說一下多線程是怎麼實現的

二、線程實現的幾種方法

在Java裏面實現線程的方法主要由三種:

1、繼承Thread類+實現run()方法

貼上代碼:

/**
 * 創建多線程,繼承Thread,重寫run方法
 */
public class ThreadPra extends Thread {
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("跑了"+i +"步");
        }
    }

    public static void main(String[] args) {
//        新生線程
        Thread t1=new Thread();
        Thread t2= new Thread();
//        啓動線程
        t1.start();
        t2.start();
    }
}

2、實現Runnable接口+run方法

public class Ticket implements Runnable {
    private int num= 50;
    @Override
    public void run() {
        while (true){
            if (num<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"搶到了"+num--);
        }
    }

    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t,"路人甲");
        Thread t2= new Thread(t,"黃牛");
        Thread t3 = new Thread(t,"程序員");//線程新生
        t1.start();//線程就緒
        t2.start();
        t3.start();
    }
}

運行結果:


可以看到這個搶佔資源的順序是隨機的。誰搶到了資源誰就先運行。

由於繼承Java的單繼承,因此推薦使用Runnable接口,優點1避免單繼承和2便於資源共享

啓動:使用靜態代理

1)創建真實角色
2)創建代理角色Thread+引用

3)代理角色.start()

缺點:不能拋出異常,沒有返回值!!

這樣的實現是不是看起來很簡單!!!

3、實現Callable接口,重寫call方法,用Future獲取返回值

使用這種方式。我們需要藉助任務調度線程池ScheduledThreadPoolExecutor,比如我們如果需要運行兩個線程,我們可以用:

ExecutorService exe = new ScheduledThreadPoolExecutor(2);

指定線程的個數爲2,

 然後創建新的線程:(這裏的Race實現了Callable接口,重寫了call方法)

Race l =new Race("tortoise",1000);
Race r = new Race("rabbit",500);
通過result.get()獲取到返回值
//        獲取返回值
        Future<Integer> res1 = exe.submit(l);
        Future<Integer> res2 = exe.submit(r);

獲取返回值,最後停止服務

exe.shutdownNow();

完整的代碼段:

ExecutorService exe = new ScheduledThreadPoolExecutor(2);
        Race l =new Race("tortoise",1000);
        Race r = new Race("rabbit",500);
//        獲取返回值
        Future<Integer> res1 = exe.submit(l);
        Future<Integer> res2 = exe.submit(r);

        Thread.sleep(4000);//4秒就停
        l.setFlag(false);
        r.setFlag(false);
        System.out.println("烏龜跑了"+ res1.get()+"");
        System.out.println("兔子跑了"+ res2.get()+"");
//        停止服務
        exe.shutdownNow();

這種方式的優點是:1)可以對外聲明異常2)有返回值。缺點是比較繁瑣

三、線程的狀態

簡單來說線程一個有五個主要的狀態:新生、就緒、運行、阻塞、死亡

那麼這幾個狀態之間怎麼轉換?以下圖來說明,注意:阻塞解除後會進入就緒狀態


1、新生狀態(new )
2、就緒狀態(線程準備就緒,待CPU分配時間片即可運行)
3、CPU給了時間片,線程進入運行狀態
4、阻塞狀態 如sleep或者等待I/O設備等資源

5、死亡狀態(兩種情況:一種是正常死亡,線程體正常執行完畢。一種是外部干涉,調用stop或者destroy強行終止,不會釋放鎖)

涉及到同步的具體的幾種狀態:


這裏的重點是怎麼樣創建線程終止線程。創建線程前面已經說過了。這裏我們看一下終止線程的幾種方法。

第一種是線程運行完畢,自然終止(死亡)。

第二種是外部干涉。外部干涉也有幾種方式。

1)線程類中定義線程體使用的標識如flag標識 2)線程體使用該標識 3)提供對外的方法改變該標識 4)外部根據條件調用該方法即可

線程阻塞的方法:1)join合併線程 2)yield暫停當前正在執行的線程對象,它是一個靜態方法,並執行其他線程 3)sleep休眠,這個是用的比較多的一個方法。調用這個方法後,線程進入休眠狀態,不會釋放鎖,此時若有新的線程則會進行等待。

sleep方法示例:

public class sleepDemo {
    public static void main(String[] args) throws InterruptedException {
        Date endTime = new Date(System.currentTimeMillis()+10*1000);
        long end =endTime.getTime();
        while (true){
//            輸出
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(endTime));
//            休眠
            Thread.sleep(1000);
//            構建下一秒的時間
            endTime = new Date(endTime.getTime()+1000);

            if(endTime.getTime() -10000> end){
                break;
            }
        }
    }
}

其他的線程的信息:

1、Thread.currentThread獲得當前線程。2、獲取名稱、設置名稱、優先級、判斷狀態

proxy.setPriority(Thread.MIN_PRIORITY);
proxy.isAlive()

四、線程同步(併發)

同步(併發):多個線程訪問同一個資源時,我們要確保這份資源的安全性。比如說銀行取錢的時候,多個人操作相同的賬戶,存取錢。如果處理不好就可能會發生不安全的問題。爲了避免這種問題,我們需要採取相應的措施。

解決併發問題所採取的主要由幾種方法:

1、給線程加鎖。java裏面採取使用synchronized關鍵字的方式。synchronized關鍵字 ->同步

Java裏面同步有同步塊和同步方法

同步塊:

synchronize(引用類型|this類.class){
//方法體
}

同步方法:

在方法中加入synchronized關鍵字,保證線程安全。

難點:線程範圍不能太大,否則浪費資源,造成等待,也不能過小,否則造成線程不安全

五、死鎖

死鎖是由什麼引起的呢?

過多的併發,造成資源等待,比如我們操作系統書裏面提到的筷子問題,每個人都在等待旁邊的人釋放筷子。這就造成了無限等待,也就造成了死鎖問題。這個時候我們就需要採取相應的措施。

解決死鎖問題的兩個個典型的方法是生產者---消費者模式、管程法

生產者消費者模式:也稱有限緩衝問題,讓生產者在緩衝區滿時休眠,等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒。

代碼:

場景:比如我們定義一個場景Movie,Player和Watch都共同使用這個資源,這就涉及到了同步問題。

生產者使用資源Movie:

/**
 * 生產者
 */
public class Player implements Runnable{
    private Movie m;
    public Player(Movie m){
        super();
        this.m=m;
    }
    @Override
    public void run() {
        for(int i= 0;i<10 ;i++){
            if(0 == i%2){
                m.play("");
            }else{
                m.play("");
            }
        }
    }
}

消費者(也使用Movie):

/**
 * 消費者
 */
public class Watch implements Runnable {
    private Movie m;
    public Watch(Movie m){
        super();
        this.m=m;
    }
    @Override
    public void run() {
        for(int i= 0;i<10 ;i++){
            m.watch();
        }

    }
}

定義兩個類共用movie資源,我們定義flag信號燈。

/**
 * 一個場景,共同的資源
 * 生產者消費者模式,信號燈法
 * wait()
 */
public class Movie {
    private String pic;

//    信號燈
//    flag -->T生產者生產,消費者等待,生產完成後通知消費者
//    flag -->F 消費者消費,生產者等待,消費完成後通知生產者

    private boolean flag = true;
    public synchronized void play(String pic){
        if (!flag){//生產者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        開始生成
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        生產完畢
        System.out.println("生產了:"+pic);
        this.pic = pic;
//        通知消費
        this.notify();
//        生產者停下
        this.flag = false;
    }
    public synchronized void watch(){
        if(flag){//消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        開始消費
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        消費完畢
        System.out.println("消費了了:"+pic);
//        通知生產
        this.notifyAll();
//        消費者停下
        this.flag = true;

    }
}

main方法:

public class App {
    public static void main(String[] args) {
        Movie m =new Movie();

//        多線程
        Player p = new Player(m);
        Watch w = new Watch(m);
//        訪問同一份資源
        new Thread(p).start();
        new Thread(w).start();
    }
}

結果:

這裏如果不使用信號燈的話,輸出的結果是一樣的。

注意:wait方法和notify和同步一起使用的!!

六、任務調度

最後提一下Timer定時器

public class TimerDemo1{
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("哈哈....");
            }
        },new Date(System.currentTimeMillis()+1000), 2000);
    }

}

我們可以用Timer來實現任務定時,比如說上面的例子,1秒之後每隔2秒打印“哈哈....”同樣還可以定其他的時間。


!!!好了,以上就是我對線程的一些相關的理解和梳理,重點就是Java中線程的創建、終止、還有線程的幾種狀態。。具體項目中用到的線程肯定不止這麼簡單,會涉及到線程池,任務調度相關的東西。未完待續。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章