1. 程式人生 > >java線程從入門到精通

java線程從入門到精通

throw 怎麽辦 -m call delay file 值類型 system task

(1)線程的簡單應用

/*
 * 雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們並沒有直接看到在哪裏加上了鎖,在哪裏釋放了鎖,
 * 為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖對象Lock。
 * 
 * Lock:
 *         void lock(): 獲取鎖。
 *         void unlock():釋放鎖。  
 * ReentrantLock是Lock的實現類.
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        
// 創建資源對象 SellTicket st = new SellTicket(); // 創建三個窗口 Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); // 啟動線程 t1.start(); t2.start(); t3.start(); } } import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; 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(); } } } }

(2)死鎖

public class MyLock {
	// 創建兩把鎖對象
	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");
				}
			}
		}
	}
}

/*
 * 同步的弊端:
 * 		A:效率低
 * 		B:容易產生死鎖
 * 
 * 死鎖:
 * 		兩個或兩個以上的線程在爭奪資源的過程中,發生的一種相互等待的現象。
 * 
 * 舉例:
 * 		中國人,美國人吃飯案例。
 * 		正常情況:
 * 			中國人:筷子兩支
 * 			美國人:刀和叉
 * 		現在:
 * 			中國人:筷子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();
	}
}

(3)案例一

/*
 * 分析:
 *         資源類:Student    
 *         設置學生數據:SetThread(生產者)
 *         獲取學生數據:GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題1:按照思路寫代碼,發現數據每次都是:null---0
 * 原因:我們在每個線程中都創建了新的資源,而我們要求的時候設置和獲取線程的資源應該是同一個
 * 如何實現呢?
 *         在外界把這個數據創建出來,通過構造方法傳遞給其他的類。
 * 
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創建資源
        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 {
    String name;
    int age;
}
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);
    }

}

(4)

/*
 * 分析:
 *         資源類:Student    
 *         設置學生數據:SetThread(生產者)
 *         獲取學生數據:GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題1:按照思路寫代碼,發現數據每次都是:null---0
 * 原因:我們在每個線程中都創建了新的資源,而我們要求的時候設置和獲取線程的資源應該是同一個
 * 如何實現呢?
 *         在外界把這個數據創建出來,通過構造方法傳遞給其他的類。
 * 
 * 問題2:為了數據的效果好一些,我加入了循環和判斷,給出不同的值,這個時候產生了新的問題
 *         A:同一個數據出現多次
 *         B:姓名和年齡不匹配
 * 原因:
 *         A:同一個數據出現多次
 *             CPU的一點點時間片的執行權,就足夠你執行很多次。
 *         B:姓名和年齡不匹配
 *             線程運行的隨機性
 * 線程安全問題:
 *         A:是否是多線程環境        是
 *         B:是否有共享數據        是
 *         C:是否有多條語句操作共享數據    是
 * 解決方案:
 *         加鎖。
 *         註意:
 *             A:不同種類的線程都要加鎖。
 *             B:不同種類的線程加的鎖必須是同一把。
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創建資源
        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 {
    String name;
    int 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 = "林青霞";//剛走到這裏,就被別人搶到了執行權
                    s.age = 27;
                } else {
                    s.name = "劉意"; //剛走到這裏,就被別人搶到了執行權
                    s.age = 30;
                }
                x++;
            }
        }
    }
}


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);
            }
        }
    }
}

(5)

/*
 * 分析:
 *         資源類:Student    
 *         設置學生數據:SetThread(生產者)
 *         獲取學生數據:GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題1:按照思路寫代碼,發現數據每次都是:null---0
 * 原因:我們在每個線程中都創建了新的資源,而我們要求的時候設置和獲取線程的資源應該是同一個
 * 如何實現呢?
 *         在外界把這個數據創建出來,通過構造方法傳遞給其他的類。
 * 
 * 問題2:為了數據的效果好一些,我加入了循環和判斷,給出不同的值,這個時候產生了新的問題
 *         A:同一個數據出現多次
 *         B:姓名和年齡不匹配
 * 原因:
 *         A:同一個數據出現多次
 *             CPU的一點點時間片的執行權,就足夠你執行很多次。
 *         B:姓名和年齡不匹配
 *             線程運行的隨機性
 * 線程安全問題:
 *         A:是否是多線程環境        是
 *         B:是否有共享數據        是
 *         C:是否有多條語句操作共享數據    是
 * 解決方案:
 *         加鎖。
 *         註意:
 *             A:不同種類的線程都要加鎖。
 *             B:不同種類的線程加的鎖必須是同一把。
 * 
 * 問題3:雖然數據安全了,但是呢,一次一大片不好看,我就想依次的一次一個輸出。
 * 如何實現呢?
 *         通過Java提供的等待喚醒機制解決。
 * 
 * 等待喚醒:
 *         Object類中提供了三個方法:
 *             wait():等待
 *             notify():喚醒單個線程
 *             notifyAll():喚醒所有線程
 *         為什麽這些方法不定義在Thread類中呢?
 *             這些方法的調用必須通過鎖對象調用,而我們剛才使用的鎖對象是任意鎖對象。
 *             所以,這些方法必須定義在Object類中。
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創建資源
        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 {
    String name;
    int age;
    boolean flag; // 默認情況是沒有數據,如果是true,說明有數據
}


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,喚醒並不表示你立馬可以執行,必須還得搶CPU的執行權。
            }
            //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
            }
        }
    }
}

(6)

public class MyRunnable implements Runnable {

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

}

/*
 * 線程組: 把多個線程組合到一起。
 * 它可以對一批線程進行分類管理,Java允許程序直接對線程組進行控制。
 */
public class ThreadGroupDemo {
    public static void main(String[] args) {
        // method1();

        // 我們如何修改線程所在的組呢?
        // 創建一個線程組
        // 創建其他線程的時候,把其他線程的組指定為我們自己新建線程組
        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());
    }
}

(7)

/*
 * 分析:
 *         資源類:Student    
 *         設置學生數據:SetThread(生產者)
 *         獲取學生數據:GetThread(消費者)
 *         測試類:StudentDemo
 * 
 * 問題1:按照思路寫代碼,發現數據每次都是:null---0
 * 原因:我們在每個線程中都創建了新的資源,而我們要求的時候設置和獲取線程的資源應該是同一個
 * 如何實現呢?
 *         在外界把這個數據創建出來,通過構造方法傳遞給其他的類。
 * 
 * 問題2:為了數據的效果好一些,我加入了循環和判斷,給出不同的值,這個時候產生了新的問題
 *         A:同一個數據出現多次
 *         B:姓名和年齡不匹配
 * 原因:
 *         A:同一個數據出現多次
 *             CPU的一點點時間片的執行權,就足夠你執行很多次。
 *         B:姓名和年齡不匹配
 *             線程運行的隨機性
 * 線程安全問題:
 *         A:是否是多線程環境        是
 *         B:是否有共享數據        是
 *         C:是否有多條語句操作共享數據    是
 * 解決方案:
 *         加鎖。
 *         註意:
 *             A:不同種類的線程都要加鎖。
 *             B:不同種類的線程加的鎖必須是同一把。
 * 
 * 問題3:雖然數據安全了,但是呢,一次一大片不好看,我就想依次的一次一個輸出。
 * 如何實現呢?
 *         通過Java提供的等待喚醒機制解決。
 * 
 * 等待喚醒:
 *         Object類中提供了三個方法:
 *             wait():等待
 *             notify():喚醒單個線程
 *             notifyAll():喚醒所有線程
 *         為什麽這些方法不定義在Thread類中呢?
 *             這些方法的調用必須通過鎖對象調用,而我們剛才使用的鎖對象是任意鎖對象。
 *             所以,這些方法必須定義在Object類中。
 * 
 * 最終版代碼中:
 *         把Student的成員變量給私有的了。
 *         把設置和獲取的操作給封裝成了功能,並加了同步。
 *         設置或者獲取的線程裏面只需要調用方法即可。
 */
public class StudentDemo {
    public static void main(String[] args) {
        //創建資源
        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; // 默認情況是沒有數據,如果是true,說明有數據

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

        // 設置數據
        this.name = name;
        this.age = age;

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

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

        // 獲取數據
        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();
        }
    }
}

(8)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
 * 線程池的好處:線程池裏的每一個線程代碼結束後,並不會死亡,而是再次回到線程池中成為空閑狀態,等待下一個對象來使用。
 * 
 * 如何實現線程的代碼呢?
 *         A:創建一個線程池對象,控制要創建幾個線程對象。
 *             public static ExecutorService newFixedThreadPool(int nThreads)
 *         B:這種線程池的線程可以執行:
 *             可以執行Runnable對象或者Callable對象代表的線程
 *             做一個類實現Runnable接口。
 *         C:調用如下方法即可
 *             Future<?> submit(Runnable task)
 *            <T> Future<T> submit(Callable<T> task)
 *        D:我就要結束,可以嗎?
 *            可以。
 */
public class ExecutorsDemo {
    public static void main(String[] args) {
        // 創建一個線程池對象,控制要創建幾個線程對象。
        // public static ExecutorService newFixedThreadPool(int nThreads)
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以執行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);
        }
    }

}

(9)

import java.util.concurrent.Callable;

//Callable:是帶泛型的接口。
//這裏指定的泛型其實是call()方法的返回值類型。
public class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
        return null;
    }

}

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
 * 多線程實現的方式3:
 *      A:創建一個線程池對象,控制要創建幾個線程對象。
 *             public static ExecutorService newFixedThreadPool(int nThreads)
 *         B:這種線程池的線程可以執行:
 *             可以執行Runnable對象或者Callable對象代表的線程
 *             做一個類實現Runnable接口。
 *         C:調用如下方法即可
 *             Future<?> submit(Runnable task)
 *            <T> Future<T> submit(Callable<T> task)
 *        D:我就要結束,可以嗎?
 *            可以。
 */
public class CallableDemo {
    public static void main(String[] args) {
        //創建線程池對象
        ExecutorService pool = Executors.newFixedThreadPool(2);
        
        //可以執行Runnable對象或者Callable對象代表的線程
        pool.submit(new MyCallable());
        pool.submit(new MyCallable());
        
        //結束
        pool.shutdown();
    }
}

(10)

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/*
 * 多線程實現的方式3:
 *      A:創建一個線程池對象,控制要創建幾個線程對象。
 *             public static ExecutorService newFixedThreadPool(int nThreads)
 *         B:這種線程池的線程可以執行:
 *             可以執行Runnable對象或者Callable對象代表的線程
 *             做一個類實現Runnable接口。
 *         C:調用如下方法即可
 *             Future<?> submit(Runnable task)
 *            <T> Future<T> submit(Callable<T> task)
 *        D:我就要結束,可以嗎?
 *            可以。
 */
public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 創建線程池對象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以執行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();
    }
}
import java.util.concurrent.Callable;

/*
 * 線程求和案例
 */
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;
    }

}

(11)

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

        // 實現Runnable接口來實現多線程
        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() {
            @Override
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println("hello" + ":" + x);
                }
            }
        }) {
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println("world" + ":" + x);
                }
            }
        }.start();
    }
}

(12)

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/*
 * 需求:在指定的時間刪除我們的指定目錄(你可以指定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);
    }
}




import java.util.Timer;
import java.util.TimerTask;

/*
 * 定時器:可以讓我們在指定的時間做某件事情,還可以重復的做某件事情。
 * 依賴Timer和TimerTask這兩個類:
 * Timer:定時
 *         public Timer()
 *         public void schedule(TimerTask task,long delay)
 *         public void schedule(TimerTask task,long delay,long period)
 *         public void cancel()
 * TimerTask:任務
 */
public class TimerDemo2 {
    public static void main(String[] args) {
        // 創建定時器對象
        Timer t = new Timer();
        // 3秒後執行爆炸任務第一次,如果不成功,每隔2秒再繼續炸
        t.schedule(new MyTask2(), 3000, 2000);
    }
}

// 做一個任務
class MyTask2 extends TimerTask {
    @Override
    public void run() {
        System.out.println("beng,爆炸了");
    }
}



import java.util.Timer;
import java.util.TimerTask;

/*
 * 定時器:可以讓我們在指定的時間做某件事情,還可以重復的做某件事情。
 * 依賴Timer和TimerTask這兩個類:
 * Timer:定時
 *         public Timer()
 *         public void schedule(TimerTask task,long delay)
 *         public void schedule(TimerTask task,long delay,long period)
 *         public void cancel()
 * TimerTask:任務
 */
public class TimerDemo {
    public static void main(String[] args) {
        // 創建定時器對象
        Timer t = new Timer();
        // 3秒後執行爆炸任務
        // 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();
    }

}

java線程從入門到精通