java基礎(三):多線程

概述

  1. 進程:是一個正在執行中的程序。每一個進程執行都有一個執行順序。該順序是每一個執行路徑,或者叫一個控制單元
  2. 線程:進程中的一個獨立的控制單元,線程在控制着進程的執行。一個進程中至少有一個線程。

創建線程

創建線程有兩種方法
1. 繼承Thread類
  創建流程:
    1.1、定義類繼承Thread
    1.2、複寫Thread類中的run方法
    1.3、調用線程的start方法(啓動線程,調用run方法)
     start方法開啓線程並調用run方法,直接調用run方法沒有開啓線程(僅僅是調用對象方法)
2. 實現Runnable接口
  創建流程:
    2.1、定義類實現Runnable接口
    2.2、覆蓋Runnable接口中的run方法
    2.3、通過Thread類建立線程對象
    2.4、將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數
    2.5、調用Thread類的start方法開啓線程

這裏寫圖片描述

注:其中stop();方法已過時

  實現方式的好處:
    實現:避免單繼承的侷限性。獨立資源。在定義線程時,建議使用實現方式

同步

  1. 多線程同步(synchronized)前提:
      必須要有兩個或者兩個以上的線程
      必須是多個線程使用同一個對象的鎖
  2. 同步的利弊
      同步好處:解決了多線程的安全問題
      同步弊端:多個線程需要判斷鎖,比較消耗資源
  3. 函數函數的鎖(this)
public class ThisLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        Thread t3 = new Thread(td);
        Thread t4 = new Thread(td);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
class TicketDemo implements Runnable{
    private int tick = 100;
    @Override
    public void run() {
        while (true) {
            saleTick();//這裏是有省略this的
        }
    }
    public synchronized void saleTick() {//可以推斷出這裏也是this鎖
        if (tick > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 賣票啦  " + tick--);
        }
    }
}

  驗證this鎖

public class ThisLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        try{Thread.sleep(10);}catch(InterruptedException e){}
        td.flag = false;
        t2.start();
    }
}
class TicketDemo implements Runnable{
    private int tick = 10;
    public boolean flag = true;
    Object obj = new Object();
    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized(obj) {
                    if (tick > 0) {
                        try {Thread.sleep(20);} catch (InterruptedException e) {}
                        System.out.println(Thread.currentThread().getName() + " if賣票啦  " + tick--);
                    }
                }
            }
        } else {
            while (true)
                saleTick();
        }
    }
    public synchronized void saleTick() {//this
        if (tick > 0) {
            try {Thread.sleep(20);} catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " else賣票啦  " + tick--);
        }
    }
}

  輸出結果爲:

Thread-0 if賣票啦  10
Thread-1 else賣票啦  9
Thread-0 if賣票啦  8
Thread-1 else賣票啦  7
Thread-0 if賣票啦  6
Thread-1 else賣票啦  5
Thread-0 if賣票啦  4
Thread-1 else賣票啦  3
Thread-0 if賣票啦  2
Thread-1 else賣票啦  1
Thread-0 if賣票啦  0

  結果出現0號票,出現安全隱患。
    分析:此多線程程序加了鎖還出現安全隱患,說明同步的兩個前提沒有滿足。
      第一個條件是必須兩個或者兩個以上的線程,此條件滿足。
      既然滿足了第一個條件,那麼肯定是第二個條件不滿足了(使用同一個鎖)。
  來驗證下this鎖會不會出現安全問題:

public class ThisLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        try{Thread.sleep(10);}catch(InterruptedException e){}
        td.flag = false;
        t2.start();
    }
}
class TicketDemo implements Runnable{
    private int tick = 10;
    public boolean flag = true;
    //Object obj = new Object();
    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized(this) {
                    if (tick > 0) {
                        try {Thread.sleep(20);} catch (InterruptedException e) {}
                        System.out.println(Thread.currentThread().getName() + " if賣票啦  " + tick--);
                    }
                }
            }
        } else {
            while (true)
                saleTick();
        }
    }
    public synchronized void saleTick() {//this
        if (tick > 0) {
            try {Thread.sleep(20);} catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " else賣票啦  " + tick--);
        }
    }
}

  輸出結果:

Thread-0 if賣票啦  10
Thread-0 if賣票啦  9
Thread-0 if賣票啦  8
Thread-0 if賣票啦  7
Thread-0 if賣票啦  6
Thread-0 if賣票啦  5
Thread-0 if賣票啦  4
Thread-1 else賣票啦  3
Thread-1 else賣票啦  2
Thread-0 if賣票啦  1

  數次運行,結果並沒有出現0號票,說明同步函數的鎖是this

  4.靜態同步函數的鎖是class對象
    靜態方法中是沒有this的,那麼鎖是誰呢?來驗證下

public class ThisLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        try{Thread.sleep(10);}catch(InterruptedException e){}
        td.flag = false;
        t2.start();
    }
}
class TicketDemo implements Runnable{
    private static int tick = 10;
    public boolean flag = true;
    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized(this) {//這裏鎖是obj(new Object())輸出的結果同樣存在安全隱患
                    if (tick > 0) {
                        try {Thread.sleep(20);} catch (InterruptedException e) {}
                        System.out.println(Thread.currentThread().getName() + " if賣票啦  " + tick--);
                    }
                }
            }
        } else {
            while (true)
                saleTick();
        }
    }
    public static synchronized void saleTick() {//this
        if (tick > 0) {
            try {Thread.sleep(20);} catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " else賣票啦  " + tick--);
        }
    }
}

  輸出結果:

Thread-0 if賣票啦  10
Thread-1 else賣票啦  9
Thread-0 if賣票啦  8
Thread-1 else賣票啦  7
Thread-0 if賣票啦  6
Thread-1 else賣票啦  5
Thread-0 if賣票啦  4
Thread-1 else賣票啦  3
Thread-0 if賣票啦  2
Thread-1 else賣票啦  1
Thread-0 if賣票啦  0

  很顯然,鎖不是this,靜態方法進內存的時候是沒有對象的,是由類直接調用的,類進內存會封裝成class類型的對象(即字節碼文件對象):類名.class 該對象的類型是Class
  那麼試試:類名.class鎖實驗下

public class ThisLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        try{Thread.sleep(10);}catch(InterruptedException e){}
        td.flag = false;
        t2.start();
    }
}
class TicketDemo implements Runnable{
    private static int tick = 10;
    public boolean flag = true;
    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized(TicketDemo.class) {
                    if (tick > 0) {
                        try {Thread.sleep(20);} catch (InterruptedException e) {}
                        System.out.println(Thread.currentThread().getName() + " if賣票啦  " + tick--);
                    }
                }
            }
        } else {
            while (true)
                saleTick();
        }
    }
    public static synchronized void saleTick() {//this
        if (tick > 0) {
            try {Thread.sleep(20);} catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " else賣票啦  " + tick--);
        }
    }
}

  輸出結果:

Thread-0 if賣票啦  10
Thread-0 if賣票啦  9
Thread-0 if賣票啦  8
Thread-0 if賣票啦  7
Thread-0 if賣票啦  6
Thread-1 else賣票啦  5
Thread-1 else賣票啦  4
Thread-1 else賣票啦  3
Thread-1 else賣票啦  2
Thread-1 else賣票啦  1

  由此可以推斷出靜態函數的鎖是類進內存會封裝成class類型的對象(即字節碼文件對象):類名.class

  多線程下的延遲加載單例模式(雙重判斷鎖)

class Single {
    private static Single s = null;
    public static Single getInstance() {
        if (s == null) {
            synchronized (Single.class) {
                if (s==null) {
                    s = new Single();
                }
            }
        }
        return s;
    }
}

  線程死鎖:同步中嵌套同步,但是鎖卻不同步,容易導致死鎖。線程死鎖時,第一個線程等待第二個線程釋放資源,而同時第二個線程又在等待第一個線程釋放資源。這裏舉一個通俗的例子:如在人行道上兩個人迎面相遇,爲了給對方讓道,兩人同時向一側邁出一步,雙方無法通過,又同時向另一側邁出一步,這樣還是無法通過。假設這種情況一直持續下去,這樣就會發生死鎖現象。 導致死鎖的根源在於不適當地運用“synchronized”關鍵詞來管理線程對特定對象的訪問。
  來看個小例子

public class DeadLockDemo {
    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t = new Thread(td);
        Thread t1 = new Thread(td);
        t.start();
        try{Thread.sleep(10);}catch(Exception e){}
        td.flag = false;
        t1.start();
    }
}
class TicketDemo implements Runnable {
    private int tick = 1000;
    Object obj = new Object();
    boolean flag = true;
    @Override
    public void run() {
        if(flag) {
            while (true) {
                synchronized (obj) {//object鎖
                    saleTick();//this鎖
                }
            }
        } else {
            while (true){
                saleTick();
            }
        }
    }
    public synchronized void saleTick() {//this鎖
        synchronized (obj) {//object鎖
            if (tick > 0) {
                try{Thread.sleep(10);}catch(Exception e){}
                System.out.println(Thread.currentThread().getName() + " 線程名稱  " + tick--);
            }
        }
    }
}

  輸出結果(每次運行結果不一樣,如果數值較小容易出現和諧狀態,可以將數值調大,即可產生死鎖)
線程死鎖結果

  再舉個簡單點的死鎖例子:

public class DeadLockDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new DeadLock(true));
        Thread t1 = new Thread(new DeadLock(false));
        t.start();
        t1.start();
    }
}
class DeadLock implements Runnable {
    private boolean flag;
    public DeadLock(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            while (true) {//如果你電腦上運行時和諧情況比較多,
            //就加個循環,只要數據夠大,在這種嵌套鎖(鎖對象不同)的情況下肯定會死鎖
                synchronized (MyLock.locka) {
                    System.out.println("if locka");
                    synchronized (MyLock.lockb) {
                        System.out.println("if lockb");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (MyLock.lockb) {
                    System.out.println("else lockb");
                    synchronized (MyLock.locka) {
                        System.out.println("else locka");
                    }
                }
            }
        }
    }
}
class MyLock {
    static Object locka = new Object();
    static Object lockb = new Object();
}

  我的輸出結果:

else lockb
if locka//到這裏就鎖住了。

線程間通訊

等待喚醒機制
  主要方法:wait();notify();notifyAll();
   都使用在同步中,因爲要對持有監視器(鎖)的線程操作,所以必須在同步中使用
  這些方法在操作同步中線程時,都必須要標識他們所操作線程中的鎖對象。只有同一個鎖上的被等待線程,可以被同一個鎖上notify喚醒。不可以對不同鎖中的線程進行喚醒。也就是說等待和喚醒必須是同一個鎖。
  示例:

/*
需求:多個線程操作同一個資源。
如,一個線程存名字和性別。另外一個線程獲取姓名和性別。
*/
public class InputOutput {
    public static void main(String[] args) {
        Person p = new Person();
        Input i = new Input(p);
        Output o = new Output(p);
        Thread t = new Thread(i);
        Thread t1 = new Thread(o);
        t.start();
        t1.start();
    }
}
class Input implements Runnable {
    private Person p;
    public Input(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        while (true) {
            System.out.println(p.name + " ... " + p.sex);
        }
    }
}
class Output implements Runnable {
    private Person p;
    public Output(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        boolean flag = true;
        while (true) {
            if (flag) {
                p.name = "張三";
                p.sex = "男";
                flag = false;
            } else {            
                p.name = "divid";
                p.sex = "women";
                flag = true;
            }
        }
    }
}
class Person {
    String name;//此爲演示代碼,工作中一般將屬性私有化,並提供get和set方法
    String sex;
}

得到結果爲:

//部分結果
張三 ... 男
divid ... women
divid ... 男
張三 ... 男
divid ... women
張三 ... 男
張三 ... women
divid ... women
張三 ... women

可以看出,多線程之間通訊存在安全隱患。修改代碼(加鎖):

public class InputOutput {
    public static void main(String[] args) {
        Person p = new Person();
        Input i = new Input(p);
        Output o = new Output(p);
        Thread t = new Thread(i);
        Thread t1 = new Thread(o);
        t.start();
        t1.start();
    }
}
class Input implements Runnable {
    private Person p;
    public Input(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        while (true) {
            synchronized(p) {//此處的鎖是用的p(因爲p是兩個線程操縱的共同數據,記住:爲保證同步,鎖的對象必須相同)
                System.out.println(p.name + " ... " + p.sex);
            }
        }
    }
}
class Output implements Runnable {
    private Person p;
    public Output(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        boolean flag = true;
        while (true) {
            synchronized(p) {
                if (flag) {
                    p.name = "張三";
                    p.sex = "男";
                    flag = false;
                } else {            
                    p.name = "divid";
                    p.sex = "women";
                    flag = true;
                }
            }
        }
    }
}
class Person {
    String name;//此爲演示代碼,工作中一般將屬性私有化,並提供get和set方法
    String sex;
}

結果:

張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
張三 ... 男
divid ... women
divid ... women
divid ... women
divid ... women
divid ... women
divid ... women
divid ... women

得到的結果雖然真確了,但是卻不是我要的:存一個,打印一個。修改後

//優化後的代碼
public class InputOutput {
    public static void main(String[] args) {
        Person p = new Person();
        new Thread(new Input(p)).start();//生產1
        new Thread(new Input(p)).start();//生產2      
        new Thread(new Output(p)).start();//消費1
        new Thread(new Output(p)).start();//消費2
    }
}
class Input implements Runnable {
    private Person p;
    public Input(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        while (true) {
            p.out();
        }
    }
}
class Output implements Runnable {
    private Person p;
    public Output(Person p) {
        this.p = p;
    }
    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x == 0) {
                p.setPerson("張三","男");
            } else {            
                p.setPerson("Divid","women");
            }
            x = (x + 1) % 2;
        }
    }
}
class Person {
    private String name;
    private String sex;
    boolean flag = false;
    //此爲演示代碼,一般將屬性私有化,並提供get和set方法
    //使用this作爲鎖的對象
    public synchronized void setPerson(String name,String sex) {
        while(this.flag) {//加入兩個村的線程都wait了。notifyAll喚醒所有線程後就會再次判斷標記,如果是if則直接往下運行,會出現生產兩次,消費一次、生產一次消費兩次的情況
            try {this.wait();} catch (InterruptedException e) {}
        }
        this.name = name;
        this.sex = sex;
        this.flag = true;
        //喚醒所有線程(flag判斷有if變成while後)(因爲線程是先進先出,假設生產2生產一次後wait了,然後生產1判斷標記後wait了,再消費1消費一次後wait了,接着喚醒一次,消費2wait了。生產2就活了,2生產一次,改變標記,喚醒一次,生產1醒了,判斷標記後又wait,所有的線程都wait了。。。就掛了。所以必須notifyAll)
        this.notifyAll();
    }
    public synchronized void out() {
        while(!this.flag) {
            try {this.wait();} catch (InterruptedException e) {}
        }
        System.out.println(this.name + " ... ... " + this.sex);
        this.flag = false;
        this.notifyAll();
    }
}

運行結果正常。

//可以copy代碼自己運行下
張三 ... 男
divid ... women
張三 ... 男
divid ... women
張三 ... 男
divid ... women

解釋爲什麼要用notifyAll而不是notify
notifyAll解釋

JDK1.5之後用lock(多個Condition對象,可以完美的控制鎖)

import java.util.concurrent.locks.*;
public class ProduceTest {
    public static void main(String[] args) {
        Resource r = new Resource();
        Consumer c = new Consumer(r);
        Producer p = new Producer(r);
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        Thread t3 = new Thread(p);
        Thread t4 = new Thread(p);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
class Consumer implements Runnable {
    private Resource r;
    public Consumer(Resource r) {
        this.r = r;
    }
    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}
class Producer implements Runnable {
    private Resource r;
    public Producer(Resource r) {
        this.r = r;
    }
    @Override
    public void run() {
        while (true) {
            r.setResource("zhangsan");
        }
    }
}
class Resource {
    private String name;
    private int count = 0;
    boolean flag = false;
    private final Lock lock = new ReentrantLock();
    private final Condition  p_cd = lock.newCondition();//多個Condition對象
    private final Condition  c_cd = lock.newCondition();//可以查看api:java.util.concurrent.locks 包裏面
    public void setResource(String name) {
        lock.lock();
        try {
            while (this.flag) {
                //生產的鎖
                try {p_cd.await();} catch (InterruptedException e) {}
            }
            this.name = name + "--" + count++;
            System.out.println(Thread.currentThread().getName()+ " ....生產者.... " + this.name );
            this.flag = true;
            //喚醒消費線程
            c_cd.signal();  
        } finally {//解鎖(必須做)
            lock.unlock();
        }
    }
    public void out() {
        lock.lock();
        try {
            while (!this.flag) {
                //消費的鎖
                try {c_cd.await();} catch (InterruptedException e) {}
            }
            System.out.println(Thread.currentThread().getName()+ " ............消費者........... " + this.name );
            this.flag = false;
            //喚醒生產的線程
            p_cd.signal();  
        } finally {//解鎖(必須做)
            lock.unlock();
        }
    }
}

創建線程的三種方式

  1. 繼承Thread類
  2. 實現Runnable接口
  3. 使用ExecutorService、Callable、Future實現有返回結果的線程
      Callable和FutureTask實現多線程
public class CallableTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<String> c = new CallableDemo();
        FutureTask<String> ft = new FutureTask<String>(c);
        FutureTask<String> ft1 = new FutureTask<String>(c);
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft1);
        t1.start();
        t2.start();
    }
}
class CallableDemo implements Callable<String> {
    private int count = 1;
    @Override
    public String call() throws Exception {
        System.out.println("CallableDemo count++ = " + (count++));
        return null;
    }
}

    ExecutorService、Callable、Future線程池實現(有返回值的線程)
參考:FelixZh

public class CallableTest {//包都是java.util.concurrent下的
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService es = Executors.newFixedThreadPool(5);
        List<Future<String>> list = new ArrayList<Future<String>>();
        for (int x=0; x<5; x++) {
            Future<String> submit = es.submit(new CallableDemo());
            list.add(submit);
        }
        es.shutdown();
        for (Future<String> f : list) {
            System.out.println(f.get().toString());
        }
    }
}
class CallableDemo implements Callable<String> {
    private int count = 1;
    @Override
    public String call() throws Exception {
        System.out.println("CallableDemo count++ = " + (count++));
        return "CallableDemo count++ = " + (count++);
    }
}

還有守護線程,停止線程,線程優先級,yield和join方法等等。

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