Java基礎學習第二十五天——多線程學習總結(二)

文檔版本 開發(fā)工具 測試平臺 工程名字 日期 作者 備注
V1.0 2016.03.31 lutianfei none

JDK5中Lock鎖的使用

  • 雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們并沒有直接看到在哪里加上了鎖,在哪里釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以后提供了一個新的鎖對象Lock。
  • Lock
    • void lock(): 獲取鎖。
    • void unlock():釋放鎖。
  • ReentrantLock是Lock的實現(xiàn)類。
public class SellTicket implements Runnable {

    // 定義票
    private int tickets = 100;

    // 定義鎖對象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 加鎖
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "張票");
                }
            } finally {
                // 釋放鎖
                lock.unlock();
            }
        }
    }

}




public class SellTicketDemo {
    public static void main(String[] args) {
        // 創(chuàng)建資源對象
        SellTicket st = new SellTicket();

        // 創(chuàng)建三個窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 啟動線程
        t1.start();
        t2.start();
        t3.start();
    }
}


死鎖問題

同步弊端

  • 效率低
  • 如果出現(xiàn)了同步嵌套,就容易產(chǎn)生死鎖問題

死鎖問題及其代碼

  • 是指兩個或者兩個以上的線程在執(zhí)行的過程中,因爭奪資源產(chǎn)生的一種互相等待現(xiàn)象

  • 同步代碼塊的嵌套案例

/*
 * 舉例:
 *         中國人,美國人吃飯案例。
 *         正常情況:
 *             中國人:筷子兩支
 *             美國人:刀和叉
 *         現(xiàn)在:
 *             中國人:筷子1支,刀一把
 *             美國人:筷子1支,叉一把
 */
public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}





public class MyLock {
    // 創(chuàng)建兩把鎖對象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}




public class DieLock extends Thread {

    private boolean flag;

    public DieLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        } else {
            synchronized (MyLock.objB) {
                System.out.println("else objB");
                synchronized (MyLock.objA) {
                    System.out.println("else objA");
                }
            }
        }
    }
}


線程間通信

  • 針對同一個資源的操作有不同種類的線程

    • 舉例:賣票有進的,也有出的。
  • 通過設置線程(生產(chǎn)者)和獲取線程(消費者)針對同一個學生對象進行操作

  • 基本版本

/*
 * 分析:
 *         資源類:Student    
 *         設置學生數(shù)據(jù):SetThread(生產(chǎn)者)
 *         獲取學生數(shù)據(jù):GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題1:按照思路寫代碼,發(fā)現(xiàn)數(shù)據(jù)每次都是:null---0
 * 原因:我們在每個線程中都創(chuàng)建了新的資源,而我們要求的時候設置和獲取線程的資源應該是同一個
 * 如何實現(xiàn)呢?
 *         在外界把這個數(shù)據(jù)創(chuàng)建出來,通過構造方法傳遞給其他的類。
 * 
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創(chuàng)建資源
        Student s = new Student();
        
        //設置和獲取的類
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        //線程類
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        //啟動線程
        t1.start();
        t2.start();
    }
}



public class SetThread implements Runnable {

    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        // Student s = new Student();
        s.name = "林青霞";
        s.age = 27;
    }

}



public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        // Student s = new Student();
        System.out.println(s.name + "---" + s.age);
    }

}


public class Student {
    String name;
    int age;
}


  • 改進版,給出了不同的數(shù)據(jù),并加入同步機制
/*
 * 分析:
 *         資源類:Student    
 *         設置學生數(shù)據(jù):SetThread(生產(chǎn)者)
 *         獲取學生數(shù)據(jù):GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題2:為了數(shù)據(jù)的效果好一些,我加入了循環(huán)和判斷,給出不同的值,這個時候產(chǎn)生了新的問題
 *         A:同一個數(shù)據(jù)出現(xiàn)多次
 *         B:姓名和年齡不匹配
 * 原因:
 *         A:同一個數(shù)據(jù)出現(xiàn)多次
 *             CPU的一點點時間片的執(zhí)行權,就足夠你執(zhí)行很多次。
 *         B:姓名和年齡不匹配
 *             線程運行的隨機性
 * 線程安全問題:
 *         A:是否是多線程環(huán)境        是
 *         B:是否有共享數(shù)據(jù)        是
 *         C:是否有多條語句操作共享數(shù)據(jù)    是
 * 解決方案:
 *         加鎖。
 *         注意:
 *             A:不同種類的線程都要加鎖。
 *             B:不同種類的線程加的鎖必須是同一把。
 */

public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                System.out.println(s.name + "---" + s.age);
            }
        }
    }
}





public class SetThread implements Runnable {

    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                if (x % 2 == 0) {
                    s.name = "林青霞";//剛走到這里,就被別人搶到了執(zhí)行權
                    s.age = 27;
                } else {
                    s.name = "劉意"; //剛走到這里,就被別人搶到了執(zhí)行權
                    s.age = 30;
                }
                x++;
            }
        }
    }
}


等待喚醒機制

  • Object類中提供了三個方法:
    • wait():等待
    • notify():喚醒單個線程
    • notifyAll():喚醒所有線程
  • 為什么這些方法不定義在Thread類中呢?
    • 這些方法的調(diào)用必須通過鎖對象調(diào)用,而我們剛才使用的鎖對象是任意鎖對象。所以,這些方法必須定義在Object類中。
  • 線程間通信的代碼改進,通過等待喚醒機制實現(xiàn)數(shù)據(jù)依次出現(xiàn)
/*
 * 問題3:雖然數(shù)據(jù)安全了,但是呢,一次一大片不好看,我就想依次的一次一個輸出。
 * 如何實現(xiàn)呢?
 *         通過Java提供的等待喚醒機制解決。
 *
 */

public class Student {
    String name;
    int age;
    boolean flag; // 默認情況是沒有數(shù)據(jù),如果是true,說明有數(shù)據(jù)
}




public class SetThread implements Runnable {

    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                //判斷有沒有
                if(s.flag){
                    try {
                        s.wait(); //t1等著,釋放鎖
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                if (x % 2 == 0) {
                    s.name = "林青霞";
                    s.age = 27;
                } else {
                    s.name = "劉意";
                    s.age = 30;
                }
                x++; //x=1
                
                //修改標記
                s.flag = true;
                //喚醒線程
                s.notify(); //喚醒t2,喚醒并不表示你立馬可以執(zhí)行,必須還得搶CPU的執(zhí)行權。
            }
            //t1有,或者t2有
        }
    }
}



public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                if(!s.flag){
                    try {
                        s.wait(); //t2就等待了。立即釋放鎖。將來醒過來的時候,是從這里醒過來的時候
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                System.out.println(s.name + "---" + s.age);
                //林青霞---27
                //劉意---30
                
                //修改標記
                s.flag = false;
                //喚醒線程
                s.notify(); //喚醒t1
            }
        }
    }
}


  • 生產(chǎn)者消費者之等待喚醒機制代碼優(yōu)化
/* 
 * 最終版代碼中:
 *         把Student的成員變量給私有的了。
 *         把設置和獲取的操作給封裝成了功能,并加了同步。
 *         設置或者獲取的線程里面只需要調(diào)用方法即可。
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創(chuàng)建資源
        Student s = new Student();
        
        //設置和獲取的類
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        //線程類
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        //啟動線程
        t1.start();
        t2.start();
    }
}


public class Student {
    private String name;
    private int age;
    private boolean flag; // 默認情況是沒有數(shù)據(jù),如果是true,說明有數(shù)據(jù)

    public synchronized void set(String name, int age) {
        // 如果有數(shù)據(jù),就等待
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 設置數(shù)據(jù)
        this.name = name;
        this.age = age;

        // 修改標記
        this.flag = true;
        this.notify();
    }

    public synchronized void get() {
        // 如果沒有數(shù)據(jù),就等待
        if (!this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 獲取數(shù)據(jù)
        System.out.println(this.name + "---" + this.age);

        // 修改標記
        this.flag = false;
        this.notify();
    }
}



public class SetThread implements Runnable {

    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                s.set("林青霞", 27);
            } else {
                s.set("劉意", 30);
            }
            x++;
        }
    }
}



public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            s.get();
        }
    }
}



線程的狀態(tài)轉(zhuǎn)換圖



線程組

  • Java中使用ThreadGroup來表示線程組,它可以對一批線程進行分類管理,Java允許程序直接對線程組進行控制。
  • 默認情況下,所有的線程都屬于主線程組。
    • public final ThreadGroup getThreadGroup()
  • 給線程設置分組
    • Thread(ThreadGroup group, Runnable target, String name)

public class ThreadGroupDemo {
    public static void main(String[] args) {
        // method1();

        // 我們?nèi)绾涡薷木€程所在的組呢?
        // 創(chuàng)建一個線程組
        // 創(chuàng)建其他線程的時候,把其他線程的組指定為我們自己新建線程組
        method2();

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

    private static void method2() {
        // ThreadGroup(String name)
        ThreadGroup tg = new ThreadGroup("這是一個新的組");

        MyRunnable my = new MyRunnable();
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, my, "林青霞");
        Thread t2 = new Thread(tg, my, "劉意");
        
        System.out.println(t1.getThreadGroup().getName());
        System.out.println(t2.getThreadGroup().getName());
        
        //通過組名稱設置后臺線程,表示該組的線程都是后臺線程
        tg.setDaemon(true);
    }

    private static void method1() {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my, "林青霞");
        Thread t2 = new Thread(my, "劉意");
        // 我不知道他們屬于那個線程組,我想知道,怎么辦
        // 線程類里面的方法:public final ThreadGroup getThreadGroup()
        ThreadGroup tg1 = t1.getThreadGroup();
        ThreadGroup tg2 = t2.getThreadGroup();
        // 線程組里面的方法:public final String getName()
        String name1 = tg1.getName();
        String name2 = tg2.getName();
        System.out.println(name1);
        System.out.println(name2);
        // 通過結果我們知道了:線程默認情況下屬于main線程組
        // 通過下面的測試,你應該能夠看到,默任情況下,所有的線程都屬于同一個組
        System.out.println(Thread.currentThread().getThreadGroup().getName());
    }
}



public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }

}


線程池

  • 程序啟動一個新線程成本是比較高的,因為它涉及到要與操作系統(tǒng)進行交互。而使用線程池可以很好的提高性能,尤其是當程序中要創(chuàng)建大量生存期很短的線程時,更應該考慮使用線程池。

    • 線程池里的每一個線程代碼結束后,并不會死亡,而是再次回到線程池中成為空閑狀態(tài),等待下一個對象來使用。
    • 在JDK5之前,我們必須手動實現(xiàn)自己的線程池,從JDK5開始,Java內(nèi)置支持線程池
  • JDK5新增了一個Executors工廠類來產(chǎn)生線程池,有如下幾個方法

    • public static ExecutorService newCachedThreadPool()
    • public static ExecutorService newFixedThreadPool(int nThreads)
    • public static ExecutorService newSingleThreadExecutor()
    • 這些方法的返回值是ExecutorService對象,該對象表示一個線程池,可以執(zhí)行Runnable對象或者Callable對象代表的線程。它提供了如下方法
      • Future<?> submit(Runnable task)
      • <T> Future<T> submit(Callable<T> task)

實現(xiàn)線程池的步驟

  • A:創(chuàng)建一個線程池對象,控制要創(chuàng)建幾個線程對象。

    • public static ExecutorService newFixedThreadPool(int nThreads)
  • B:創(chuàng)建Runnable實例

    • 可以執(zhí)行Runnable對象或者Callable對象代表的線程
    • 做一個類實現(xiàn)Runnable接口。
  • C:提交Runnable實例

    • Future<?> submit(Runnable task)
    • <T> Future<T> submit(Callable<T> task)
  • D:關閉線程池

    • shutdown()
  • 例子:

public class ExecutorsDemo {
    public static void main(String[] args) {
        // 創(chuàng)建一個線程池對象,控制要創(chuàng)建幾個線程對象。
        // public static ExecutorService newFixedThreadPool(int nThreads)
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以執(zhí)行Runnable對象或者Callable對象代表的線程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        //結束線程池
        pool.shutdown();
    }
}



public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }

}


多線程程序?qū)崿F(xiàn)方案3:創(chuàng)建線程池方式

  • 實現(xiàn)Callable接口

    • 好處:
      • 可以有返回值
      • 可以拋出異常
    • 弊端:
      • 代碼比較復雜,所以一般不用
  • 多線程求和案例


public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 創(chuàng)建線程池對象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以執(zhí)行Runnable對象或者Callable對象代表的線程
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        Future<Integer> f2 = pool.submit(new MyCallable(200));

        // V get()
        Integer i1 = f1.get();
        Integer i2 = f2.get();

        System.out.println(i1);
        System.out.println(i2);

        // 結束
        pool.shutdown();
    }
}


public class MyCallable implements Callable<Integer> {

    private int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }

}


匿名內(nèi)部類方式使用多線程

  • new Thread(){代碼…}.start();
  • New Thread(new Runnable(){代碼…}).start();
/*
 * 匿名內(nèi)部類的格式:
 *         new 類名或者接口名() {
 *             重寫方法;
 *         };
 *         本質(zhì):是該類或者接口的子類對象。
 */
public class ThreadDemo {
    public static void main(String[] args) {
        // 繼承Thread類來實現(xiàn)多線程
        new Thread() {
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + x);
                }
            }
        }.start();

        // 實現(xiàn)Runnable接口來實現(xiàn)多線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + x);
                }
            }
        }) {
        }.start();

        // 更有難度的
        new Thread(new Runnable() { //Runnable 接口的run方法
            @Override
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println("hello" + ":" + x);
                }
            }
        }) {
            public void run() { //子類對象本身的run方法,默認走的是此方法。
                for (int x = 0; x < 100; x++) {
                    System.out.println("world" + ":" + x);
                }
            }
        }.start();
    }
}



定時器

  • 定時器是一個應用十分廣泛的線程工具,可用于調(diào)度多個定時任務以后臺線程的方式執(zhí)行。在Java中,可以通過Timer和TimerTask類來實現(xiàn)定義調(diào)度的功能
  • Timer
    • public Timer()
    • public void schedule(TimerTask task, long delay)
    • public void schedule(TimerTask task,long delay,long period)
  • TimerTask
    • public abstract void run()
    • public boolean cancel()
  • 開發(fā)中
    • Quartz是一個完全由java編寫的開源調(diào)度框架。
  • 單次定時案例
public class TimerDemo {
    public static void main(String[] args) {
        // 創(chuàng)建定時器對象
        Timer t = new Timer();
        // 3秒后執(zhí)行爆炸任務
        // t.schedule(new MyTask(), 3000);
        //結束任務
        t.schedule(new MyTask(t), 3000);
    }
}

// 做一個任務
class MyTask extends TimerTask {

    private Timer t;
    
    public MyTask(){}
    
    public MyTask(Timer t){
        this.t = t;
    }
    
    @Override
    public void run() {
        System.out.println("beng,爆炸了");
        t.cancel();
    }

}


  • 案例:定時刪目錄
/*
 * 需求:在指定的時間刪除我們的指定目錄(你可以指定c盤,但是我不建議,我使用項目路徑下的demo)
 */

class DeleteFolder extends TimerTask {

    @Override
    public void run() {
        File srcFolder = new File("demo");
        deleteFolder(srcFolder);
    }

    // 遞歸刪除目錄
    public void deleteFolder(File srcFolder) {
        File[] fileArray = srcFolder.listFiles();
        if (fileArray != null) {
            for (File file : fileArray) {
                if (file.isDirectory()) {
                    deleteFolder(file);
                } else {
                    System.out.println(file.getName() + ":" + file.delete());
                }
            }
            System.out.println(srcFolder.getName() + ":" + srcFolder.delete());
        }
    }
}

public class TimerTest {
    public static void main(String[] args) throws ParseException {
        Timer t = new Timer();

        String s = "2014-11-27 15:45:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = sdf.parse(s);

        t.schedule(new DeleteFolder(), d);
    }
}


多線程面試題

  • 1:多線程有幾種實現(xiàn)方案,分別是哪幾種?
    • 兩種。
    • 繼承Thread類
    • 實現(xiàn)Runnable接口

擴展一種:實現(xiàn)Callable接口。這個得和線程池結合。


  • 2:同步有幾種方式,分別是什么?
    • 兩種。
    • 同步代碼塊
    • 同步方法


  • 3:啟動一個線程是run()還是start()?它們的區(qū)別?

    • 啟動用:start();
    • run():封裝了被線程執(zhí)行的代碼,直接調(diào)用僅僅是普通方法的調(diào)用
    • start():啟動線程,并由JVM自動調(diào)用run()方法
  • 4:sleep()和wait()方法的區(qū)別

    • sleep():必須指時間;不釋放鎖。
    • wait():可以不指定時間,也可以指定時間;釋放鎖。
  • 5:為什么wait(),notify(),notifyAll()等方法都定義在Object類中

    • 因為這些方法的調(diào)用是依賴于鎖對象的,而同步代碼塊的鎖對象是任意鎖。而Object代碼是任意的對象,所以,定義在這里面。
  • 6:線程的生命周期圖

    • 新建 -- 就緒 -- 運行 -- 死亡
    • 新建 -- 就緒 -- 運行 -- 阻塞 -- 就緒 -- 運行 -- 死亡


最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1. 多線程概述 1.1 多線程引入 由上圖中程序的調(diào)用流程可知,這個程序只有一個執(zhí)行流程,所以這樣的程序就是單線...
    JackChen1024閱讀 453評論 0 1
  • 該文章轉(zhuǎn)自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍閱讀 7,465評論 3 87
  • 前言 多線程并發(fā)編程是Java編程中重要的一塊內(nèi)容,也是面試重點覆蓋區(qū)域,所以學好多線程并發(fā)編程對我們來說極其重要...
    嘟爺MD閱讀 7,412評論 21 272
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的,為什么轉(zhuǎn)載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,936評論 12 45
  • 今日吃了好幾驚。 第一驚:打開秒拍自己錄視頻玩,攝像頭切換前置:愛瑪,二師兄,你咋混到這世上了?哪個眼瘸的找了你?...
    芳芳樂分享閱讀 316評論 3 0

友情鏈接更多精彩內(nèi)容