多線程這塊,平時用的框架里都封裝好了,只有寫批處理和工具包時用過幾次.現在水平僅僅限於會用的程度,需要全面深入學習多線程.
主要內容:創建線程,啟動線程,控制線程,多線程的同步,線程池,使用線程安全的集合類
16.1.1 線程和進程
線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程.線程可以擁有自己的堆棧,程序計數器和局部變量,但不擁有系統資源,它與父進程的其他線程共享該進程所擁有的全部資源.因為多個線程共享父進程的全部資源,因此編程更加方便,帶也需要更加小心.
16.2 線程的創建和啟動
繼承Thread類創建線程類
public class FirstThread extends Thread{ private int i; @Override public void run(){ for(i=0;i<100;i++){ System.out.println(getName() + " " +i); } } public static void main(String[] args){ FirstThread xx = new FirstThread(); xx.setName("xx"); xx.start(); for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"------ " +i); if(i==20){ new FirstThread().start(); new FirstThread().start(); } } } }
實現Runnable接口創建線程類
public class RunnableThread implements Runnable{ private int i;
@Override public void run(){ for (;i<100;i++){ System.out.println(Thread.currentThread().getName()+"wwwwwwww "+i); } } public static void main(String[] args){ for (int i = 0;i<100;i++){ // System.out.println(Thread.currentThread().getName()+" "+i); if(i==20){ RunnableThread st = new RunnableThread(); new Thread(st,"新線程1").start(); new Thread(st,"新線程2").start(); } } } }
這兩種方法的區別:Runnable對象僅僅作為Thread對象的target,Runnable實現類里包含的run()方法僅作為線程執行體.而實際的線程對象依然是Thread實例,只是改Thread線程負責執行其target的run()方法.
第一個例子里面每個thread各操作一個對象,所以打印的i是互不相干的(各自打印100個數)
第二個例子里面兩個thread操作的是同一個對象,所以打印的i是相互關聯的(一共打印了100個數).
有返回值的線程方法
public class CallableThread { public static void main(String[] atgs){ // CallableThread rt = new CallableThread(); FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{ int i= 0; for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+" 1循環變量i的值:"+i); } return i; }); for(int i = 0;i<100;i++){ System.out.println(Thread.currentThread().getName()+" 2循環變量i的值:"+i); if (i==20){ new Thread(task,"有返回值的線程").start(); } } try{ System.out.println("子線程的返回值:"+task.get()); }catch(Exception ex) { ex.printStackTrace(); } } }
線程的生命周期
有新建(new),就緒(Runnable),運行(Running),阻塞(Blocked)和死亡(Dead)5種狀態.
join()和join(long millis),等待join的線程執行完了,被join的線程繼續執行
public class FirstThread extends Thread{ private int i; @Override public void run(){ for(i=0;i<100;i++){ System.out.println(getName() + " " +i); } } public static void main(String[] args) throws InterruptedException{ FirstThread xx = new FirstThread(); xx.setName("xx"); xx.start(); xx.setPriority(MIN_PRIORITY); for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"------ " +i); if(i==20){ FirstThread thread1 = new FirstThread(); thread1.start(); thread1.setPriority(MAX_PRIORITY); FirstThread thread2 = new FirstThread(); thread2.start(); thread2.join(); new FirstThread().start(); } } } }
調用Thread對象的setDaemon(true)方法可將指定線程設置成后台線程.
要將某個線程設置為后台線程,必須在start()之前調用
線程睡眠sleep(long millis)
sleep()方法比yield()方法有更好的可移植性,通常不建議使用yield()方法來控制並發線程的執行.
16.4.5 改變線程的優先級
Thread類提供了setPriority(int newPriority)和getPriority()來設置
和返回指定線程的優先級.10最大,1最小,正常為5
16.5 線程同步
1.synchronized(obj){
}
2.synchronized修飾某個方法或代碼塊,但不能修飾構造器和成員變量
重點解析:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
Java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執行該段代碼。
一、當兩個並發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
四、第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
五、以上規則對其它對象鎖同樣適用.
延伸:單線程環境下應該使用StringBuilder來保證較好的性能,當需啊喲保證多線程安全時,
就 應該使用StringBuffer
16.5.5 同步鎖(Lock)
Lock是控制多個線程對共享資源進行訪問的工具.通常,鎖提供了對共享資源的獨占訪問,每次只能有一個
線程對Lock對象加鎖,線程開始訪問共享資源之前,應該先獲得Lock對象.
//定義鎖對象 private final ReentrantLock lock = new ReentrantLock(); //定義需要保證線程安全的方法 public void m(){ lock.lock(); try{ //需要保證線程安全的代碼 //...method body } //使用finally塊來保證釋放鎖 finally{ lock.unlock(); } }
ReentrantLock鎖具有可重入性,也就是說,一個線程可以對已被加鎖的ReentrantLock鎖再次加鎖
ReentrantLock對象會維持一個計數器來追蹤lock()方法的嵌套調用,線程在每次調用lock()加鎖后,必須顯示調用unlock()來釋放鎖,所以一段被鎖包不戶的代碼可以調用另一個被相同鎖保護的方法.
死鎖
package learnThread; class A { public synchronized void foo( B b ) { System.out.println("當前線程名: " + Thread.currentThread().getName() + " 進入了A實例的foo()方法" ); // ① try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("當前線程名: " + Thread.currentThread().getName() + " 企圖調用B實例的last()方法"); // ③ b.last(); } public synchronized void last() { System.out.println("進入了A類的last()方法內部"); } } class B { public synchronized void bar( A a ) { System.out.println("當前線程名: " + Thread.currentThread().getName() + " 進入了B實例的bar()方法" ); // ② try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("當前線程名: " + Thread.currentThread().getName() + " 企圖調用A實例的last()方法"); // ④ a.last(); } public synchronized void last() { System.out.println("進入了B類的last()方法內部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主線程"); // 調用a對象的foo方法 a.foo(b); System.out.println("進入了主線程之后"); } public void run() { Thread.currentThread().setName("副線程"); // 調用b對象的bar方法 b.bar(a); System.out.println("進入了副線程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); // 以dl為target啟動新線程 new Thread(dl).start(); // 調用init()方法 dl.init(); } } ----------------------------------------------------------------------- 當前線程名: 主線程 進入了A實例的foo()方法 當前線程名: 副線程 進入了B實例的bar()方法 當前線程名: 副線程 企圖調用A實例的last()方法 當前線程名: 主線程 企圖調用B實例的last()方法
16.6 線程通信
包括
16.6.1傳統的線程通信,用this或者是被鎖的對象來調用.
wait(),notify(), notifyAll()
16.6.2使用Condition控制線程通信,調用Lock對象的newCondition()方法即可.
await(),signal(),signalAll()
16.6.3 使用阻塞隊列(BlockingQueue)控制線程通信
BlockingQueue是Queue的子接口,特征為:當生產者線程試圖向BlockingQueue中放入元素時,如果該對壘已滿,
則該線程被阻塞;當消費者線程試圖從BlockingQueue中取出元素時,如果該隊列已空,則該線程被阻塞.
put(E e)
take(E e)
16.7 線程組合未處理的異常
16.8線程池
當程序中需要創建大量生存期很短暫的線程時,應該考慮使用線程池
與線程連接池類似的是,線程池在系統啟動時即創建大量空閑的線程,程序將一個Runnable對象或Callable對象傳給線程池,線程池就會啟動一個線程來執行他們的run()或call方法,當run()或call()方法執行結束后,該線程並不會立即死亡,而是再次返回線程池中成為空閑狀態,等待執行下一個Runnable對象的run()或call()方法.
步驟如下:
1.調用Executors類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池
2.創建Runnable實現類或Callable實現類的實例,作為線程執行任務.
3.調用ExecutorService對象的submit()方法來提交Runnable實例或Callable實例.
4.當不想提交任何任務時,調用ExecutorService對象的shutdown()方法來關閉線程池.
public static void threadPoolTest(){ // 創建足夠的線程來支持4個CPU並行的線程池 // 創建一個具有固定線程數(6)的線程池 ExecutorService pool = Executors.newFixedThreadPool(4); // 使用Lambda表達式創建Runnable對象 Runnable target = () -> { for (int i = 0; i < 1 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值為:" + i); } }; // 向線程池中提交兩個線程 pool.submit(target); pool.submit(target); pool.submit(target); pool.submit(target); try { Thread.sleep(10000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } pool.submit(target); pool.submit(target); // 關閉線程池 pool.shutdown(); }
16.8.2 Java 8 增強的ForkJoinPool
為多cpu和多核准備的線程池類
16.9 線程相關類(這個很重要)
16.9.1 ThreadLocal 類
ThreadLocal為每一個使用該變量的線程都提供了一個變量值的副本,使每一個線程都可以獨立地改變自己的副本,而不會和其他線程
的副本沖突.
T get()
void remove()
void set(T value)
class Account { /* 定義一個ThreadLocal類型的變量,該變量將是一個線程局部變量 每個線程都會保留該變量的一個副本 */ private ThreadLocal<String> name = new ThreadLocal<>(); // 定義一個初始化name成員變量的構造器 public Account(String str) { this.name.set(str); // 下面代碼用於訪問當前線程的name副本的值 System.out.println("---" + this.name.get()); } // name的setter和getter方法 public String getName() { return name.get(); } public void setName(String str) { this.name.set(str); } } class MyTest extends Thread { // 定義一個Account類型的成員變量 private Account account; public MyTest(Account account, String name) { super(name); this.account = account; } public void run() { // 循環10次 for (int i = 0 ; i < 10 ; i++) { // 當i == 6時輸出將賬戶名替換成當前線程名 if (i == 6) { account.setName(getName()); } // 輸出同一個賬戶的賬戶名和循環變量 System.out.println(account.getName() + " 賬戶的i值:" + i); } } } public class ThreadLocalTest { public static void main(String[] args) { // 啟動兩條線程,兩條線程共享同一個Account Account at = new Account("初始名"); /* 雖然兩條線程共享同一個賬戶,即只有一個賬戶名 但由於賬戶名是ThreadLocal類型的,所以每條線程 都完全擁有各自的賬戶名副本,所以從i == 6之后,將看到兩條 線程訪問同一個賬戶時看到不同的賬戶名。 */ new MyTest(at , "線程甲").start(); new MyTest(at , "線程乙").start (); } }
結果如下:
---初始名 null 賬戶的i值:0 null 賬戶的i值:1 null 賬戶的i值:0 null 賬戶的i值:2 null 賬戶的i值:1 null 賬戶的i值:3 null 賬戶的i值:2 null 賬戶的i值:3 null 賬戶的i值:4 null 賬戶的i值:4 null 賬戶的i值:5 null 賬戶的i值:5 線程乙 賬戶的i值:6 線程乙 賬戶的i值:7 線程甲 賬戶的i值:6 線程乙 賬戶的i值:8 線程乙 賬戶的i值:9 線程甲 賬戶的i值:7 線程甲 賬戶的i值:8 線程甲 賬戶的i值:9
16.9.2 包裝線程不安全的集合
ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是線程不安全的.
如果程序中有多個線程可能訪問以上這些集合,就可以使用Collection提供的類方法把這些結婚包裝成線程安全的集合.
HashMap<String,String> m = (HashMap<String, String>) Collections.synchronizedMap(new HashMap<String, String>());
16.9.3 線程安全的集合類
習題
1.寫兩個線程,一個線程打印1~52,另一個線程打印A~Z,打印順序為12A34B56C...5152Z.該習題需要利用多線程通信的知識.
沒看標准答案,下面這個是我自己寫的,單個線程可以打印1~52或者A~Z,兩個可以順序打印,但是感覺實現方式很牽強,待改善.
class NumberTest implements Runnable{ Exercise exercise; public NumberTest(Exercise exercise){ this.exercise = exercise; } @Override public void run() { // TODO Auto-generated method stub for(int i = 1;i<27;i++){ try { exercise.printSomething(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class Letter implements Runnable{ Exercise exercise; public Letter(Exercise exercise){ this.exercise = exercise; } @Override public void run() { // TODO Auto-generated method stub for(int i = 'A';i<='Z';i++){ try { exercise.printSomething(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class Exercise{ int mark = 0; public synchronized void printSomething(int i) throws InterruptedException{ if(i>64){ System.out.println((char)i); notify(); wait(5); }else{ System.out.print(i*2-1); System.out.print(i*2); wait(5); notify(); } } } public class ThreadTest { public static void main(String[] args) throws InterruptedException{ Exercise exercise = new Exercise(); NumberTest numberTest = new NumberTest(exercise); Letter letter = new Letter(exercise); new Thread(numberTest).start(); Thread.sleep(2); new Thread(letter).start(); } }
把代碼整理一下,如下
public class Test { public synchronized void printSomething(int i){ if (i > 64) { System.out.println((char) i); notify(); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.print(i * 2 - 1); System.out.print(i * 2); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } notify(); } } public static void main(String[] args){ Test test = new Test(); Runnable num = () -> { for (int i = 1; i < 27; i++) { test.printSomething(i); } }; Runnable letter = () -> { for (int i = 'A'; i <= 'Z'; i++) { test.printSomething(i); } }; new Thread(num).start(); new Thread(letter).start(); } }