java基礎之 多線程

總結學習,我認爲是一個非常好的學習方法。

寫在前面【摘】:

作爲一名Java開發人員,不管作爲面試官,還是被面試的對象,甚至是兩者兼有。Java線程技術的考察,勢必成爲整個面試過程的重點之一。分析一下原因,不難發現,實際工作當中,涉及到的Java應用幾乎全是多線程,單線程Java應用微乎其微。如何管理好多線程的調度,比如線程的安全問題,是Java應用實現高效並行運行的關鍵點之一,也是擺在大多數Java初學者的難題。


多線程

我從以下幾個方面進行知識總結:

一、概述

操作系統可以同時執行多個任務,每個任務就是進程;進程可以同時執行多個任務,每個任務就是線程。

注:現代的操作系統都支持多進程的併發,但在具體的實現細節上可能因爲硬件和操作系統的不同而採用不同的策略:如共用式、搶佔式等。

        一般,進程包含如下三個特徵:

       (1)獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。

       (2)動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,這些概念在程序中都是不具備的。

       (3)併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會相互影響。

二、線程創建和啓動的幾種方法

1、繼承Thread 類

      new MyThread().start();

2、實現Runnable 接口

      MyRunnable mr = new MyRunnable();

      new Thread(mr , "新線程").start();

兩種方法的優缺點:

a、實現Runnable接口創建多線程

優點:

(1)還可以實現其他接口,繼承其他類。

(2)在這種情況下,多個線程可以共享同一個target 對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。

缺點:

編程稍稍複雜,如果需要訪問當前線程,則必須使用Thread.currentThread() 方法。

b、採用繼承Thread 類的方式創建多線程的

優點:編寫簡單,如果需要訪問當前線程,則無須使用Thread.currentThread() 方法,直接使用this 即可獲得當前線程。

缺點:已經繼承了Thread 類,就不能再繼承其他父類。

綜上所述:一般採用 Runnable  接口創建多線程。


三、線程的生命週期、線程的控制

三、線程同步

1、爲什麼要線程同步:

線程有可能和其他線程共享一些資源,比如,內存,文件,數據庫等。
當多個線程同時讀寫同一份共享資源的時候,可能會引起衝突。這時候,我們需要引入線程“同步”機制,即各位線程之間要有個先來後到,不能一窩蜂擠上去搶作一團。
線程同步的真實意思和字面意思恰好相反。線程同步的真實意思,其實是“排隊”:幾個線程之間要排隊,一個一個對共享資源進行操作,而不是同時進行操作。

2、實現線程同步的幾種方法:

(1)同步監視器(Synchronized)

<1>從使用方式上,Synchronized包含兩種用法:①Synchronized 方法和 ②Synchronized 塊。

①、前者在方法聲明前加synchronized關鍵字,如:


public synchronized void accessVal(int newVal);
用來控制對共享資源的訪問。同一時間,僅允許一個線程操作該方法,其它線程排隊。

②、後者在代碼塊前加synchronized關鍵字,如:

synchronized(syncObject) {
  //允許訪問控制的代碼
}
和前者一樣,同一時間,僅允許一個線程進入該代碼塊。
兩者的唯一區別是,Synchronized 方法對應的鎖對象是this,而Synchronized 塊對應的鎖對象可以自由指定。

<2>從作用域的角度,Synchronized也分爲兩種:實例對象和類對象。synchronized aMethod(){}是一個實例方法,這就意味着,對於同一個對象,同一時間,僅有可能被一個線程調用,其它線程排隊。而對於與不同的對象,其它線程仍然能訪問該方法。所不同的是,對於類對象而言,情況則完全不同。synchronized static aStaicMethod(){}是一個類方法,這就意味着,同一時間,該方法只能被一個線程調用,其它的線程沒有任何機會。除非當前線程執行完操作或終止,釋放線程鎖。

<3>Synchronized 經典實戰:生產者與消費者【摘】

線程的同步最經典的案例莫過於生產者與消費者問題。我們的例子將圍繞它展開。生產者與消費者指的是兩個線程共享一個公共的固定大小的緩衝區。其中一個是生產者線程,用於將“產品”放入緩衝區;另一個是消費者線程,用於從緩衝區取出“產品”。問題出現在緩衝區已滿,生產者還想添加“產品”,其解決辦法是讓生產者此時休眠,待消費者從緩衝區取走一個或者多個“產品”再喚醒。同樣地,當緩衝區已空,消費者還想取出“產品”,此時也可以讓消費者休眠,待生產者放入一個或者多個數據時再喚醒它。

package thread.synchronizedTest;
/**
 * 假設緩衝區的長度爲1,這裏的“產品”對應一個整數。該緩衝區提供讀、寫操作。對應的Buffer類
 *
 */
public class Buffer {
int n;
boolean hasValue = false;


synchronized int get() {
while (!hasValue)
try {
wait();


} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println("Get: " + n);
hasValue = false;
notify();
return n;
}


synchronized void put(int n) {
while (hasValue)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.n = n;
hasValue = true;
System.out.println("Put: " + n);
notify();
}
}

package thread.synchronizedTest;
/**
 * 生產者線程
 */
class Producer implements Runnable {
Buffer q;
Producer(Buffer q) {
this.q = q;
new Thread(this, "Producer").start();
}


public void run() {
int i = 0;
while (true) {
q.put(i++);
}
}
}

package thread.synchronizedTest;
/**
 * 消費者線程
 *
 */
public class Consumer implements Runnable {
Buffer q;
Consumer(Buffer q) {
this.q = q;
new Thread(this, "Consumer").start();
}


public void run() {
while (true) {
q.get();
}
}
}

package thread.synchronizedTest;
/**
 * Main 方法
 */
public class Sample {

public static void main(String args[]) {
Buffer q = new Buffer();
new Consumer(q);
new Producer(q);
}
}

(2)同步鎖(Lock)

Lock 提供了比 synchronized 方法和 synchronized 代碼塊更廣泛的鎖定操作,Lock允許實現更靈活的結構,可以具有差別很大的屬性,並且支持多個相關的對象。

Lock是控制多個線程對共享資源進行訪問的哦給你工具。通常,鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象。

class X {
// 定義鎖對象
private final ReentrantLock lock = new ReentrantLock();
// ...
// 定義需要保證線程安全的方法
public void m() {
// 加鎖
lock.lock();
try{
// 需要保證線程安全的代碼
// ... method body
}
// 使用finally 塊來保證釋放鎖
finally{
lock.unlock();
}
}
}

注:某些鎖可能允許對共享資源併發訪問,如 ReadWriteLock(讀寫鎖), Lock、ReadWriteLock 是 java5 提供的兩個接口,併爲Lock 提供了 ReentrantLock (可重入鎖),爲ReedWriteLock 提供了ReentrantReadWriteLock 實現類。

ReentrantLock鎖具有可重入性,也就是說,一個線程可以對已被加鎖的ReentrantLock 鎖再次加鎖,ReentrantLcok 對象會維持一個計數器來追蹤Lock() 方法的嵌套調用,線程在每次調用lock() 加鎖後,必須顯式調用unlock() 來釋放鎖,所以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法。

(3)死鎖

當兩個線程相互等待對方釋放同步監視器時就會發生死鎖,java 虛擬機沒有監測,也沒有採取措施來處理死鎖情況,所以多線程編程時應該採取措施避免死鎖出現。

實例:

package deadLock;


public class A {
public synchronized void foo(B b) {
System.out.println("當前線程名:" + Thread.currentThread().getName() + "進入了A實例的foo()方法");
try{
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前線程名:" + Thread.currentThread().getName() + "企圖調用B實例的last() 方法");
b.last();
}
public synchronized void last() {
System.out.println("進入了A類last() 方法內部");
}
}

package deadLock;


public class B {
public synchronized void bar(A a) {
System.out.println("當前線程名:" + Thread.currentThread().getName() + "進入了B實例的bar() 方法");
try{
Thread.sleep(200);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前線程名:" + Thread.currentThread().getName() + " 企圖調用A實例的last()方法");
a.last();
}
public synchronized void last() {
System.out.println("進入了B 類的last()方法內部");
}
}

package deadLock;


public class DeadLock implements Runnable {
A a = new A();
B b = new B();

public void init() {
Thread.currentThread().setName("主線程");
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();
// 以d1 爲 target 啓動新線程
new Thread(dl).start();
// 調用init()方法
dl.init();
}
}

注意:由於Thread 類的suspend() 方法也很容易導致死鎖,所以java 不再推薦使用該方法來暫停線程的執行。

注:synchronized 與 Lock的區別:

1.synchronized安全,Lock不安全安全與不安全是指是否會引起死鎖。
synchronized關鍵字是安全的,因爲它在臨界區開始處加鎖,臨界區結束處解鎖,這是由虛擬機控制的,不會有錯。但是使用Lock時要注意,不能忘記解鎖,否者會死鎖。
2.Lock要比synchronized效率高
3.Lock最大優勢在於它分爲讀鎖和寫鎖,而synchronized沒有
Lock的讀鎖允許多個線程一起讀,當然,不允許一邊讀一邊寫,也允許多個線程同時寫;而synchronized只能讓一個線程操作,無論讀寫。


四、線程通訊【摘】

(1)線程的協調運行
場景:用2個線程,這2個線程分別代表存款和取款。——現在系統要求存款者和取款者不斷重複的存款和取款的動作,而且每當存款者將錢存入賬戶後,取款者立即取出這筆錢。不允許2次連續存款、2次連續取款。實現上述場景需要用到Object類,提供的wait、notify和notifyAll三個方法,這3個方法並不屬於Thread類。但這3個方法必須由同步監視器調用,可分爲2種情況:
A、對於使用synchronized修飾的同步方法,因爲該類的默認實例this就是同步監視器,所以可以在同步中直接調用這3個方法。
B、對於使用synchronized修改的同步代碼塊,同步監視器是synchronized後可括號中的對象,所以必須使用括號中的對象調用這3個方法
方法概述:
一、wait方法:導致當前線程進入等待,直到其他線程調用該同步監視器的notify方法或notifyAll方法來喚醒該線程。wait方法有3中形式:無參數的wait方法,會一直等待,直到其他線程通知;帶毫秒參數的wait和微妙參數的wait,這2種形式都是等待時間到達後甦醒。調用wait方法的當前線程會釋放對該對象同步監視器的鎖定。
二、notify:喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會隨機選擇喚醒其中一個線程。只有當前線程放棄對該同步監視器的鎖定後(用wait方法),纔可以執行被喚醒的線程。
三、notifyAll:喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,才能執行喚醒的線程。
(2)、條件變量控制協調
如果程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象來保證同步,則系統中不存在隱式的同步監視器對象,也不能使用wait、notify、notifyAll方法來協調進程的運行。
當使用Lock對象同步,Java提供一個Condition類來保持協調,使用Condition可以讓那些已經得到Lock對象卻無法組合使用,爲每個對象提供了多個等待集(wait-set),這種情況下,Lock替代了同步方法和同步代碼塊,Condition替代同步監視器的功能。Condition實例實質上被綁定在一個Lock對象上,要獲得特定的Lock實例的Condition實例,調用Lock對象的newCondition即可。
Condition類方法介紹:
一、await:類似於隱式同步監視器上的wait方法,導致當前程序等待,直到其他線程調用Condition的signal方法和signalAll方法來喚醒該線程。 該await方法有跟多獲取變體:long awaitNanos(long nanosTimeout),void awaitUninterruptibly()、awaitUntil(Date daadline)
二、signal:喚醒在此Lock對象上等待的單個線程,如果所有的線程都在該Lock對象上等待,則會選擇隨機喚醒其中一個線程。只有當前線程放棄對該Lock對象的鎖定後,使用await方法,纔可以喚醒在執行的線程。
三、signalAll:喚醒在此Lock對象上等待的所有線程。只有當前線程放棄對該Lock對象的鎖定後,纔可以執行被喚醒的線程。

實例:

package thread.Condition;


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
// 顯示定義  Lock 對象
private final Lock lock = new ReentrantLock();
// 獲得指定Lock 對象對應的 Condition
private final Condition cond = lock.newCondition();
// 封裝賬戶編號、賬戶餘額的兩個成員變量
private String accountNo;
private double balance;
// 標誌賬戶中是否已有存款的旗幟
private boolean flag = false;
public Account() {}
// 構造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
// 賬戶餘額不允許修改,所以,只提供getter方法
public double getBalance() {
return balance;
}
public void draw(double drawAmount) {
// 加鎖
lock.lock();
try{
// 如果flag 爲假,表名賬戶中還沒有人存錢進去,取錢方法阻塞
if(!flag) {
cond.await();
}else{
// 執行取錢操作
System.out.println(Thread.currentThread().getName() + " 取錢: " + drawAmount);
balance -= drawAmount;
System.out.println("賬戶餘額爲:" + balance);
// 將標識賬戶是否已有存款的旗標設爲 false
flag = false;
// 喚醒其他線程
cond.signalAll();
}
} catch(InterruptedException e) {
e.printStackTrace();
}
// 使用finally 塊兒來釋放鎖
finally{
lock.unlock();
}
}
public void deposit(double depositAmount) {
lock.lock();
try{
// 如果flag 爲真,表明賬戶中已有人存錢進去,存錢方法阻塞
if(flag) {
cond.await();
}else{
// 執行存款操作
System.out.println(Thread.currentThread().getName() + "存款: " + depositAmount);
balance += depositAmount;
System.out.println("賬戶餘額爲:" + balance);
// 將表示賬戶是否已有存款的標誌設爲true
flag = true;
// 喚醒其他線程
cond.signalAll();
}
} catch(InterruptedException e){
e.printStackTrace();
}
// 使用finally 塊來釋放鎖
finally{
lock.unlock();
}
}
// 下面兩個方法根據accountNo 來重寫hashCode() 和 equals() 方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

package thread.Condition;
/**
 * 存錢
 *
 */
public class DepositThread extends Thread{
// 模擬用戶帳戶
private Account account;
// 當前存款線程所希望存的錢數
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
// 重複100 次執行存款操作
public void run() {
for(int i = 0; i < 100; i++) {
account.deposit(depositAmount);
}
}
}

package thread.Condition;
/**
 * 取錢的線程類 
 */
public class DrawThread extends Thread{
// 模擬用戶帳戶
private Account account;
// 當前取錢線程所希望取的錢數
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 重複100 次執行取錢操作
public void run() {
for(int i = 0; i < 100; i++) {
account.draw(drawAmount);
}
}
}

package thread.Condition;


public class DrawTest {
public static void main(String[] args) {
// 創建一個賬戶
Account acct = new Account("1234567", 1000);
new DrawThread("取錢者", acct, 800).start();
new DepositThread("存錢人", acct, 800).start();
}
}
(3)、使用管道流
線程通信使用管道流,管道流有3種形式:
PipedInputStream、PipedOutputStream、PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,它們分別是管道流的字節流、管道字符流和新IO的管道Channel。
管道流通信基本步驟:
A、使用new操作法來創建管道輸入、輸出流
B、使用管道輸入流、輸出流的connect方法把2個輸入、輸出流連接起來
C、將管道輸入、輸出流分別傳入2個線程
D、2個線程可以分別依賴各自的管道輸入流、管道輸出流進行通信


注:文章部分內容參考:http://tech.it168.com/a2012/0131/1305/000001305256.shtml


發佈了28 篇原創文章 · 獲贊 21 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章