《java開發實戰經典》李興華——C9. 多執行緒

Monica_xxt發表於2018-10-09

一、程式與執行緒

1.程式:程式是程式的一次動態執行過程,包括從程式碼載入、執行、到執行完畢。

2.執行緒:執行緒是比程式更小的執行單位,一個程式可以包括多個執行緒,這些執行緒可以同時存在、執行。

程式和執行緒都是實現併發的基本單位。

Java程式執行時最少2執行緒:main執行緒 和 垃圾回收執行緒。

二、Java中執行緒的實現

Java中實現多執行緒的兩種手段:1.繼承Thread類  2.實現Runnable介面。不管用哪種方法,都要依靠Thread類來實現。

1.繼承Thread類:

1)概念:Thread類是在java.lang包中定義的,繼承時必需覆寫run()方法,此方法為執行緒的主體。

2)例項:

直接調run()方法實際上沒有啟動執行緒
要啟動執行緒,需要呼叫start()方法
注意:一個例項化物件只能呼叫一次start()方法,否則會出現IllegalThreadStateException異常

public class Test {
	public static void main(String args[]) {
		MyThread a1 = new MyThread("first");
		MyThread a2 = new MyThread("second");
		a1.run();
		a2.run();
	}	
}

 
class MyThread extends Thread{
	private String name;
	public MyThread(String name) {
		this.name = name;
	}
	@Override
	public void run() {
		for(int i = 0;i<10;i++) {
			System.out.println(name +"執行,i = "+i);
		}
	}

//此時未啟動執行緒,按順序執行,先列印a1的全部,再列印a2。


public class Test {
	public static void main(String args[]) {
		MyThread a1 = new MyThread("first");
		MyThread a2 = new MyThread("second");
		a1.start();
		a2.start();
	}	
}

 
class MyThread extends Thread{
	private String name;
	public MyThread(String name) {
		this.name = name;
	}
	@Override
	public void run() {
		for(int i = 0;i<10;i++) {
			System.out.println(name +"執行,i = "+i);
		}
	}
//此時啟動執行緒,a1,a2交替列印。

2.實現Runnable介面:

1)概念:

Runnable介面中只定義了一個抽象方法 public void run()。因此可以通過覆寫run()方法實現多執行緒。

2)例項:

實際上還是藉助於Thread類,然後呼叫start()方法。
因為Thread類提供了public Thread(Runnable r)和public Thread(Runnable r,String name)兩個構造方法。

public class Test {
	public static void main(String args[]) {
		MyThread a1 = new MyThread("first");
		MyThread a2 = new MyThread("second");
		Thread  t1 = new Thread(a1);
		Thread t2 = new Thread(a2);
		t1.start();
		t2.start();
	}	
}
 
class MyThread implements Runnable{
	private String name;
	public MyThread(String name) {
		this.name = name;
	}
	@Override
	public void run() {
		for(int i = 0;i<10;i++) {
			System.out.println(name +"執行,i = "+i);
		}
	}
}

3.Thread類和Runnable介面:

1)實際上,Thread和Runnable的子類都同時實現了Runnable介面,之後將Runnable的子類例項放到了Thread類中。

2)繼承Thread不適用於多個執行緒資源共享;實現Runnable介面則可以在多執行緒間資源共享。

//1.資源不共享
public class Test {
	public static void main(String args[]) {
		MyThread t1 = new MyThread();
		MyThread t2 = new MyThread();
		t1.start();
		t2.start();
	}	
}

class MyThread extends Thread{
	private int ticket = 5;
	@Override
	public void run() {
		for(int i = 0;i<10;i++) {
			if(ticket>0) {
				System.out.println("賣票:ticket = "+ticket-- );
			}
		}
	}
}
//賣票:ticket = 5
//賣票:ticket = 5
//賣票:ticket = 4
//賣票:ticket = 4
//賣票:ticket = 3
//賣票:ticket = 2
//賣票:ticket = 1
//賣票:ticket = 3
//賣票:ticket = 2
//賣票:ticket = 1


//2.資源共享
public class Test {
	public static void main(String args[]) {
		MyThread my = new MyThread();
		new Thread(my).start();
		new Thread(my).start();
	}	
}

 
class MyThread implements Runnable{
	private int ticket = 5;
	@Override
	public void run() {
		for(int i = 0;i<10;i++) {
			if(ticket>0) {
				System.out.println("賣票:ticket = "+ticket-- );
			}
		}
	}
}
//賣票:ticket = 5
//賣票:ticket = 4
//賣票:ticket = 3
//賣票:ticket = 2
//賣票:ticket = 1

3.)實現Runnable對比繼承Thread的優勢:

       適合多個相同程式程式碼的執行緒去處理同一資源的情況。

       可以避免由於Java的單繼承帶來的侷限。

       增強了程式的健壯性,程式碼能夠被多個執行緒共享,程式碼與資料是獨立的。

因此,建議用實現Runnable

三、執行緒的狀態

建立、就緒、執行、阻塞、終止。

四、執行緒操作的相關方法

1)設定當前執行緒名稱,不設時預設分配,格式為Thread-X

     Thread.currentThread().setName("執行緒A")

2)獲取當前執行緒名稱

     Thread.currentThread().getName();

3)判斷執行緒是否啟動,返回true&false

      t.isAlive()

4)執行緒的強制執行,執行緒強制執行期間,其他執行緒無法執行,必須等此執行緒完成。

      t.join()

5)執行緒的休眠

      Thread.sleep(500) //休眠500ms

6)中斷執行緒

      t.interrupt()

7) 後臺執行緒

       t.setDaemon()

8)執行緒的優先順序

     Java中所有執行緒在執行前都會保持在就緒狀態,此時哪個執行緒優先順序高,它就會有可能先被執行。

     setPriority()可以設定執行緒的優先順序,Java中有3中優先順序:

     

      示例:

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<3;i++) {
			try {
				Thread.sleep(500);//休眠5s
			}catch(Exception e) {
			}finally {
				System.out.println(Thread.currentThread().getName()
						+"執行,i ="+i);//輸出執行緒名稱
			}
		}
	}
}

public class Test {
	public static void main(String args[]) {
		//例項化3個執行緒
		Thread t1 = new Thread(new MyThread(),"執行緒A");
		Thread t2 = new Thread(new MyThread(),"執行緒B");
		Thread t3 = new Thread(new MyThread(),"執行緒C");
		//給三個執行緒設定不同優先順序
		t1.setPriority(Thread.MIN_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t3.setPriority(Thread.NORM_PRIORITY);
		//啟動三個執行緒
		t1.start();
		t2.start();
		t3.start();
	}	
}

//執行結果:
執行緒B執行,i =0
執行緒C執行,i =0
執行緒A執行,i =0
執行緒B執行,i =1
執行緒C執行,i =1
執行緒A執行,i =1
執行緒B執行,i =2
執行緒C執行,i =2
執行緒A執行,i =2

 注意:main方法的優先順序是NORM_PRIORITY。getPriority()獲得當前執行緒優先順序。

9)執行緒的禮讓

      Thread.currentThread().yield();//執行緒禮讓:將本執行緒暫停,讓其他執行緒先執行。

五、執行緒操作範例

設計一個執行緒操作類,產生3個執行緒物件,並設定3個執行緒的休眠時間,分別是:

  執行緒A:休眠10秒
  執行緒B:休眠20秒
  執行緒C:休眠30秒  

1.實現方法一:

class MyThread extends Thread{
	private int sleepTime;
	public int getSleepTime() {
		return sleepTime;
	}
	public void setSleepTime(int sleepTime) {
		this.sleepTime = sleepTime;
	}
	public MyThread(String name,int sleepTime) {
		super(name);
		this.setSleepTime(sleepTime);
	}
	@Override
	public void run() {
		try {
			Thread.sleep(sleepTime);
		}catch(Exception e){
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+",休眠"+sleepTime+"豪秒。");
	}
}
public class Test {
	public static void main(String args[]) {
		MyThread t1 = new MyThread("執行緒A",10000);
		MyThread t2 = new MyThread("執行緒B",20000);
		MyThread t3 = new MyThread("執行緒C",30000);
		t1.start();
		t2.start();
		t3.start();
	}	
}

2.實現方法二:

class MyThread implements Runnable{
	private int sleepTime;
	
	public int getSleepTime() {
		return sleepTime;
	}

	public void setSleepTime(int sleepTime) {
		this.sleepTime = sleepTime;
	}
	
	public MyThread(int st) {
		this.setSleepTime(st);
	}
	@Override
	public void run() {
		try {
			Thread.sleep(sleepTime);
		}catch(Exception e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+",休眠"+this.getSleepTime()+"毫秒。");
	}
}

public class Test {
	public static void main(String args[]) {
		Thread t1 = new Thread(new MyThread(1000),"執行緒A");
		Thread t2 = new Thread(new MyThread(2000),"執行緒B");
		Thread t3 = new Thread(new MyThread(3000),"執行緒C");
		t1.start();
		t2.start();
		t3.start();
	}	
}

六、同步與死鎖

1.問題的引出:

通過runnable介面實現多執行緒,意味著屬性會被資源共享。如:產生3個執行緒,賣5張票:

class MyThread implements Runnable{
	private int ticket = 5;
	
	@Override
	public void run() {
		for(int i =0;i<10;i++) {
			if(ticket>0) {
				try {
					Thread.sleep(300);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("剩餘票數:"+ticket--);
			}
		}
		
	}
}

public class Test {
	public static void main(String args[]) {
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
	}	
}
//執行結果(每次執行結果不相同):
剩餘票數:5
剩餘票數:4
剩餘票數:5
剩餘票數:3
剩餘票數:2
剩餘票數:3
剩餘票數:1
剩餘票數:0
剩餘票數:-1

2.問題的解決——同步

所謂同步,指多個操作在同一時間段內只有一個執行緒進行,其他執行緒要等待此執行緒完成後才可以繼續進行。

同步操作有兩種方式:同步程式碼塊和同步方法。

1)同步程式碼塊

      synchronized(同步物件){

            需要同步的程式碼;

      }

class MyThread implements Runnable{
	private int ticket = 5;
	public void run() {
		for(int i =0;i<10;i++) {
			synchronized (this) {//一般用this指代當前物件
				if(ticket>0) {
					try {
						Thread.sleep(300);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("剩餘票數:"+ticket--);
				}
			}
		}
		
	}
}

public class Test {
	public static void main(String args[]) {
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
	}	
}

2)同步方法

      synchronized 返回值 方法名(引數列表){
      }

class MyThread implements Runnable{
	private int ticket = 5;
	public void run() {
		for(int i =0;i<10;i++) {
			this.saleTicket();
		}
	}
	
	synchronized void saleTicket() {
		if(ticket>0) {
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("剩餘票數:"+ticket--);
		}
	}
}

public class Test {
	public static void main(String args[]) {
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
	}	
}

3.死鎖

死鎖,即兩個執行緒都在等彼此先完成,因此儘量避免過多的同步。

七、執行緒操作案例——生產者和消費者

生產者生產兩種產品:
 *  1.李興華,Java講師
 *  2.mldn,www.mldnjava.cn
消費者依次取走生產者生產的產品。
理想情況下:生產者完整生產一個產品,消費者取一次,然後生產者再生產下一個產品。
應避免的問題:
 *     1.產品只生產完一部分就被取走
 *     2.生產者生產多次後,消費者才開始取走;或消費者取走後,還沒等到新的產品,就再次取了已取過的資料。

一、最簡單的想法

/*
 * 定義一個產品類
 */
class Product{
	
	private String info1 = "李興華";
	private String info2 = "Java講師";	
	public String getInfo1() {
		return info1;
	}
	public void setInfo1(String info1) {
		this.info1 = info1;
	}
	public String getInfo2() {
		return info2;
	}
	public void setInfo2(String info2) {
		this.info2 = info2;
	}
	
}

/*
 * 定義一個生產者類
 */
class Producer implements Runnable{
	
	private Product p = null;
	
	public Producer(Product p) {
		this.p = p;
	}
	private boolean pF = false;//生產標誌,true時生產第一種,false時生產第二種
	@Override
	public void run() {
		for(int i =0;i<10;i++) {
			if(pF) {
				this.p.setInfo1("李興華");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.p.setInfo2("java講師");
				pF = false;
			}else {
				this.p.setInfo1("mldn");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.p.setInfo2("www.mldnjava.cn");
				pF = true;
			}
		}
	}
}

/*
 * 定義一個消費者類
 */
class Consumer implements Runnable{

	private Product p = null;
	public Consumer(Product p) {
		this.p = p;
	}
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(p.getInfo1()+p.getInfo2());
		}
	}
}

/*
 * 程式入口
 */
public class Test{
	public static void main(String[] args) {
		Product p = new Product();
		Producer per = new Producer(p);
		Consumer cer = new Consumer(p);
		new Thread(per).start();
		new Thread(cer).start();
	}
}

此時結果:

李興華,www.mldnjava.cn
mldn,Java講師
李興華,www.mldnjava.cn
mldn,Java講師
李興華,www.mldnjava.cn
mldn,Java講師
mldn,Java講師
李興華,www.mldnjava.cn
李興華,Java講師
李興華,Java講師

此時問題:

 一種產品的內容沒設定完整就被取走了。因此應該先保證一個產品能設定完全部內容。

解決方法:

1.將設定產品內容的方法進行同步(synchronized set)
2.將獲取產品內容的方法進行同步(synchronized get)
3.生產者生產時直接呼叫set
4.消費者消費時直接呼叫get

二、加入同步,修改以下程式碼

/*
 * 定義一個產品類
 */
class Product{
	
	private String info1 = "李興華";
	private String info2 = "Java講師";	
	public String getInfo1() {
		return info1;
	}
	public void setInfo1(String info1) {
		this.info1 = info1;
	}
	public String getInfo2() {
		return info2;
	}
	public void setInfo2(String info2) {
		this.info2 = info2;
	}
	
	public synchronized void set(String info1,String info2) {
		this.setInfo1(info1);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.setInfo2(info2);
	}
	
	public synchronized void get() {
		try {
			Thread.sleep(300);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(this.getInfo1()+this.getInfo2());
	}
}

/*
 * 定義一個生產者類
 */
class Producer implements Runnable{
	
	private Product p = null;
	
	public Producer(Product p) {
		this.p = p;
	}
	private boolean pF = false;//生產標誌,true時生產第一種,false時生產第二種
	@Override
	public void run() {
		for(int i =0;i<10;i++) {
			if(pF) {
				this.p.set("李興華", "java講師");
				pF = false;
			}else {
				this.p.set("mldn", "www.mldnjava.cn");
				pF = true;
			}
		}
	}
}

/*
 * 定義一個消費者類
 */
class Consumer implements Runnable{

	private Product p = null;
	public Consumer(Product p) {
		this.p = p;
	}
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			this.p.get();
		}
	}
}

此時結果:

mldn,www.mldnjava.cn
李興華,Java講師
mldn,www.mldnjava.cn
mldn,www.mldnjava.cn
李興華,Java講師
mldn,www.mldnjava.cn
mldn,www.mldnjava.cn
李興華,Java講師
李興華,Java講師
李興華,Java講師

此時問題:

產品資訊錯亂解決了,但是還存在重複讀寫問題。

解決方法:

引入等待喚醒機制。
在Product類中加一個標誌位,為true時生產,為false時消費。

三、引入等待喚醒機制,修改以下程式碼

/*
 * 定義一個產品類
 */
class Product{
	
	private String info1 = "李興華";
	private String info2 = "Java講師";
	private boolean flag = false;
	public String getInfo1() {
		return info1;
	}
	public void setInfo1(String info1) {
		this.info1 = info1;
	}
	public String getInfo2() {
		return info2;
	}
	public void setInfo2(String info2) {
		this.info2 = info2;
	}
	
	public synchronized void set(String info1,String info2) {
		if(!flag) {
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		this.setInfo1(info1);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.setInfo2(info2);
		flag = false;
		super.notify();
		
	}
	
	public synchronized void get() {
		if(flag) {
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
			
		try {
			Thread.sleep(300);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(this.getInfo1()+this.getInfo2());
		flag = true;
		super.notify();
		
		
	}
}

此時結果符合預期,生產一個消費一個。

但是注意:休眠時間的設定,最好在消費前給夠生產的時間。

八、執行緒的生命週期

執行緒生命週期種的方法已經學過的有:

1.new Thread()       2.start()    3.run()     4.sleep()     5.wait()

接下來介紹:

6.suspend() :暫時掛起執行緒     7.resume() :恢復被掛起的執行緒    8.stop():停止執行緒

這三種方法不推薦使用,因為容易產生死鎖問題。

既然不推薦使用,那我們怎麼停止一個執行緒呢?

——  通過設定標誌位的方式,如下:

class MyThread implements Runnable{

	private boolean flag = true;
	
	@Override
	public void run() {
		int i=0;
		while(this.flag) {
			System.out.println(Thread.currentThread().getName()+"執行,i="+(i++));
			if(i > 4) {
				this.stop();
			}
		}
	}
	
	public void stop() {
		this.flag = false;
	}
}

public class Test{
	public static void main(String[] args) {
		MyThread my = new MyThread();
		Thread t = new Thread(my);
		t.start();
	}
}

 

相關文章