05_多線程

一、多線程簡介

(1)、進程:是一個正在執行中的程序

             每一個進程執行都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元

(2)、線程:就是進程中一個獨立的控制單元

             線程在控制着進程的執行。

            一個進程中至少要有一個線程

           開啓多個線程是爲了同時運行多部分代碼

           每一個線程都有自己運行的內容,這個內容可以稱爲線程要執行的任務

           線程運行的程序在main方法中,該線程就是主線程。

(3)、多線程的利與弊:

多線程的好處:解決了多部分同時運行的問題

多線程的弊端:線程太多回到的效率降低

(4)、多線程的隨機性:

其實應用程序的執行都是cpu在做着快速的切換完成的 ,這個切換是隨機的,我們可以形象把多線程的運行行爲在互相搶奪cpu執行權。這就是多線程的隨機性。

jvm啓動時就啓動了多個線程,至少有兩個線程可以分析出來

1、執行main函數的線程

該線程的任務代碼都定義在main函數中

2、負責垃圾回收的線程

二、線程的創建

(1)、方式一: 繼承Thread類

步驟:

1、定義類繼承Thread.

2、複寫Thread類中的run方法。

      目的:將自定義代碼存儲在run方法。讓線程運行。

3、調用線程的start方法。

      該方法有兩個作用 :啓動線程,調用run方法。


爲什麼要覆蓋run方法呢?

      Thread類用於描述線程。

      該類就定義了一個功能,用於存儲線程要運行的代碼,該存儲功能就是run方法。

      也即是說run方法是用於存儲線程要運行的代碼。

public class Demo {
	public static void main(String[] args) {
		/*
		 * 創建線程的目的就是爲了開啓一條執行路徑,去運行的代碼和其他代碼實現同時運行
		 * 
		 * 而運行的代碼就是這個執行路徑的任務
		 * 
		 * jvm創建的主線程的任務都定義在了主函數中。
		 * 
		 * 而自定義的線程它的任務在哪呢? Thread類用於描述線程,線程是需要任務的,所以Thread類也對任務的描述
		 * 這個任務就是通過Thread類的run方法來體現的, 也就是說,run方法封裝自定義線程運行任務的函數
		 * 
		 * run方法中定義就是線程要運行的任務代碼
		 * 
		 * 開啓線程就是運行指定代碼,所以只有繼承Thread類,並複寫run方法。 將運行的代碼定義在run方法中即可
		 */
		Demo4 d4 = new Demo4("李四");
		Demo4 d5 = new Demo4("張三");
		//<strong><span style="color:#ff0000;"> d4.run();//僅僅是對象調用方法。而線程創建了,並沒有執行</span></strong>
		<span style="color:#ff0000;">d4.start();// 開啓線程調用run方法</span>
		d5.start();
		System.out.println("結束了這個線程。。" + Thread.currentThread().getName());
	}
}

/**
 * 繼承方式
 */
class Demo4 extends Thread {
	private String name;

	Demo4(String name) {
		<span style="color:#ff0000;">super(name);// 給定義的線程命名</span>
	}

	// 重寫run方法
	public void run() {
		// 循環測試
		for (int x = 0; x < 10; x++) {
			System.out.println(name + ".." + x + "..."
					+ Thread.currentThread().getName());
		}
	}
}

新建狀態(New):新創建了一個線程對象。

就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

沒有執行資格的情況下是凍結狀態。sleep(), 時間到 wait(),notify()

有執行資格的狀態叫做臨時狀態。

既有資格又有執行權運行狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。


線程對象以及名稱:

原來線程都有自己默認的名稱。

Thread-編號 該編號從0開始。

Thread 對象的setName() getName();方法

線程初始化名稱:

構造方法 super(name);

Thread currentThread():獲取當前正在執行的線程對象的引用。

(2)、方式二:實現Runnable接口

        1、步驟:

 a、定義類實現Runnable接口

 b、覆蓋接口Runnable中的run方法,將線程的任務代碼封裝到run方法中

 c、通過Thread類創建線程對象,並將Runnable接口的子類對象作爲Thread類的構造函數的參數進行傳遞

        爲什麼呢?因爲線程的任務都封裝在Runnable接口子類對象的run方法中。 所以要線程對象的start方法開啓線程

d、調用Thread類中的start方法,開啓線程並調用Runnnable接口。

        2、實現Runnable接口的好處:

 a、將線程的任務從線程的子類中分離出來,進行單獨的封裝

       按照面向對象的思想將任務封裝成對象

b、避免了java單繼承的侷限性

      所以創建線程的第二種方式較爲常用

public class Test9 {
	public static void main(String[] args) {
		TextThread t = new TextThread();
		Thread t1 = new Thread(t);//線程1
		Thread t2 = new Thread(t);//線程2
		t1.start();
		t2.start();
	}
}
/**
 * 繼承方式,實現Runnable接口
 * @author Administrator
 *
 */
class TextThread implements Runnable {
 //重寫run方法
	@Override
	public void run() {
		// TODO Auto-generated method stub
		show();
	}
    //測試方法
	public void show() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "..." + i);

		}
	}

}
(3)、兩者的區別:

繼承Thread:線程代碼存放在Thread子類run方法中

實現Runnnable:線程代碼存在接口子類的run方法。

在定義線程時,建議使用實現方式。

三、線程的安全問題

(1)、發現問題

public class Test9 {
	public static void main(String[] args) {
		Ticket1 t = new Ticket1();
		// 開啓四個線程
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);

		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Ticket1 implements Runnable {
	private int ticket = 10;// 共享數據

	public void run() {
		while (ticket > 0) {
			if (ticket > 0) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "。。sale。。"
						+ ticket--);// 有負數的存在,安全隱患
			}
		}
	}
}

通過分析,發現,打印出0,-1,-2等錯票

1、問題原因:

      當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。

      導致共享數據的錯我。

(2)、解決辦法:

對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。

Java對於多線程的安全問題提供了專業的解決方式。

這種專業的解決方式就是:同步代碼快

 哪些代碼需要同步,就看哪些語句在操作共享數據。

synchronized(對象){

需要被同步的代碼

}

程序示例:

public class Test9 {
	public static void main(String[] args) {
		//開啓4個線程
		Ticket1 t = new Ticket1();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Ticket1 implements Runnable {
	private int ticket = 100;

	Object obj=new Object();
	public void run() {

		while (true) {
		//同步代碼塊,加鎖,對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。
			synchronized (this) {
				if (ticket > 0) {
					try {
						Thread.sleep(10);//讓線程休眠
					} catch (Exception e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()
							+ "。。sale。。" + ticket--);//沒有出現負數票
				}
			}
		}
	}
}

1、同步代碼快的理解:

        對象如同鎖,持有鎖的線程可以在同步中執行。

        沒有持有鎖的線程即使獲取cpu的執行權,也進不去,因爲沒有獲取鎖。

        舉例:火車上的衛生間,裏面有人時,門就會鎖住,別人就進不去。只有人出來了,把門打開,其他人才能進去。

2、同步的前提:

      a、必須要有兩個或者兩個以上的線程。

      b、必須是多個線程使用同一個鎖。

            必須保證同步中只能有一個線程在運行。

3、同步的利與弊:

       好處:解決多線程的安全問題。

       弊端:多個線程需要判斷鎖,較爲消耗資源。允許消耗範圍內的。

(3)、同步的兩種表現形式

1、是同步代碼塊(如上程序段所示)

2、是同步函數。把synchronized作爲修飾符放在函數上。

程序示例:


public class Test10 {
	public static void main(String[] args) {
		Tickets t = new Tickets();
		// 創建4個線程賣票,並開啓
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Tickets implements Runnable {
	private int ticket = 1000;// 票數

	// 複寫run方法調用show
	public void run() {
		while (ticket > 0) {
			this.show();
		}
	}

	// 同步函數所持有的鎖是this
	public synchronized void show() {
		if (ticket > 0) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "...sale..."
					+ ticket--);
		}
	}
}

(4)、同步函數和同步代碼塊:

       1、同步函數使用的鎖是this

驗證:使用兩個線程來買票。

一個線程在同步代碼塊中。

一個線程在同步函數中。

都在執行買票動作。

       2、同步代碼塊使用的鎖是任意對象。

(5)、靜態同步函數的鎖

靜態的同步函數使用的鎖是:該函數所屬字節碼文件對象

可以使用getClass方法獲取,也可以用“當前類名.class“表示

靜態的同步方法使用的鎖是該方法所在類的字節碼對象。

驗證方法如下:

public class Test10 {
	public static void main(String[] args) {
		Tickets t = new Tickets();
		// 創建4個線程賣票,並開啓
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		/*
		 * Thread t3=new Thread(t); Thread t4=new Thread(t);
		 */
		t1.start();// t1一開啓跑到同步代碼塊中。,開啓這個線程不一定立即執行。處於臨時狀態,有可能執行下面一句
		t.flag = false;// 在t2開啓之前,把標識變爲false;
		try {
			Thread.sleep(10);// 主線程停止10毫秒,只能是t1在運行。過了時間段,可能執行下面的語句
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		t2.start();// t2一開啓跑到同步函數中。
		/*
		 * t3.start(); t4.start();
		 */
	}
}

class Tickets implements Runnable {
	private static int tick = 1000;// 票數
	// 複寫run方法調用show
	Object obj = new Object();
	boolean flag = true;

	@Override
	public void run() {
		// TODO Auto-generated method stub
		if (flag) {
			while (true) {
				// 同步代碼塊
				// synchronized(obj),存在安全問題
				synchronized (Test10.class) {
					if (tick > 0) {
						try {
							Thread.sleep(10);
							System.out.println("同步代碼塊");
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			}
		} else {
			while (true) {
				show();
			}
		}
	}

	// 同步函數
	public synchronized void show() {
		if (tick > 0) {
			try {
				Thread.sleep(10);
				System.out.println("同步函數");
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

(6)、懶漢式單利模式:

加入同步爲了解決線程安全問題

加入雙重判斷是爲了解決效率問題

此處是懶漢式示例:

public class Test10 {
	public static void main(String[] args) {
		System.out.println("hello");
	}
}

class SingleDemo {
	private static SingleDemo s = null;// 共享數據,多個線程併發訪問getInstance(),有可能存在安全問題,多條語句操作

	private SingleDemo() {// 私有構造函數
	}

	public static SingleDemo getInstance() {
		if (s == null) {
			synchronized (SingleDemo.class) {// 鎖是字節碼文件對象
				if (s == null) {
					s = new SingleDemo();// 對象延遲加載
				}
			}
		}
		return s;
	}
}

(7)、線程死鎖

兩個對象互相依賴,所以死鎖!示例代碼如下:

public class Test10 implements Runnable {
	public int flag = 1;
	static Object o1 = new Object(), o2 = new Object();//兩個鎖
	public void run() {
		System.out.println("flag=" + flag);
		//兩個鎖相持不下
		if (flag == 1) {
			synchronized (o1) {
				try {
					Thread.sleep(500);
				} catch (Exception e) {
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("1");
				}
			}
		}
		if (flag == 0) {
			synchronized (o2) {
				try {
					Thread.sleep(500);
				} catch (Exception e) {
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("0");
				}
			}
		}
	}

	public static void main(String[] args) {
		Test10 td1 = new Test10();
		Test10 td2 = new Test10();
		//定義標識
		td1.flag = 1;
		td2.flag = 0;
		//兩個線程開啓
		Thread t1 = new Thread(td1);
		Thread t2 = new Thread(td2);
		t1.start();
		t2.start();
	}
}

四、線程間的通訊

(1)、概述:其實就是多個線程在操作同一個資源,但是操作的動作不同

程序示例:

class Res {
	String name;
	String sex;
	boolean flag = false;// 標記是否有 資源
}

// 添加類,實現Runnable接口
class Input implements Runnable {
	private Res r;// 資源對象

	public Input(Res r) {// 關聯資源對象
		this.r = r;
	}

	// 重寫run方法
	public void run() {
		// TODO Auto-generated method stub
		int x = 0;// 這裏也必須要加線程,操作共享數據,同一個鎖,可以是資源對象
		while (true) {
			synchronized (r) {
				if (r.flag) {
					try {
						r.wait();// 等待
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				if (x == 0) {
					r.name = "麗麗";
					r.sex = "女";
				} else {
					r.name = "mike";
					r.sex = "man";// 線程結束後,有可能還能搶到cpu執行權
				}
				x = (x + 1) % 2;
				r.flag = true;
				r.notify();// 喚醒線程池中的最早wait的線程。
			}
		}
	}
}

// 輸出類實現Runnable接口
class Output implements Runnable {
	private Res r;

	public Output(Res r) {// 關聯資源對象
		this.r = r;
	}

	// 重寫run方法
	public void run() {
		// TODO Auto-generated method stub
		while (true) {
			synchronized (r) {
				if (!r.flag) {
					try {
						r.wait();// 等待,取消了執行資格
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				System.out.println(r.name + "..." + r.sex);
				r.flag = false;
				r.notify();// 叫醒線程池中的最早線程
			}
		}
	}
}

public class Test {
	public static void main(String[] args) {
		Res r = new Res();
		// 形象的比喻 有一堆煤,有兩個大卡車,一個進的,一個出的,把煤放到大卡車上,把卡車放到高速公路上。
		Input in = new Input(r);
		Output out = new Output(r);
		// 開啓兩個線程
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

(2)、分析問題:

a、以上的代碼還是有問題的,按理說應該是存一個打印一個這樣是比較靠譜的。上面的情況是一大片一大片的男,或者女。爲什麼出現這種情況?

輸入的線程如果獲得了cpu執行權,它存了一個值後,其他線程進不來,這個時候出了同步,output,input都有可能搶到cpu執行權。所以輸入有可能還會搶到,前面的值就回被覆蓋掉了。當某一時刻,執行權被搶走了,輸出被搶到了,他也可能把一個值打印多遍,所以造成了上面的情況。cpu切換造成的。現在需求是這樣的,添加一個,取出一個,這樣纔是最靠譜的。爲了滿足條件需求,我們要做的是,在資源中加入一個標記,默認false;輸入線程在往裏面添加數據時,判斷標記,false則存入,存完後,輸入線程可能還持有執行權,將標記改爲真,代表裏面有數據了。爲true時,不能在存入了,這個時候,讓輸入線程等着不動,wait()放棄了執行資格;當取走了之後,才能醒,notify()。

當output具備執行權的時候,開始輸出,之前也要進行判斷,如果true,取出,打印,變爲false還持有執行權,回來之後,爲false,wati(),叫醒 input,input等的時候,再把output叫醒。等待喚醒機制。

wait();

notity();

notityAll();

都是用在同步中。因爲要對持有監視器(鎖)的線程操作。

所以要使用同步中,因爲只有同步才具有鎖。


b、爲什麼這些操作線程的方法要定義在Object中呢?

因爲這些方法在操作同步線程時,都必須要標識它們所操作線程只有的鎖。

只有同一個鎖上的被等待線程,可以被同一個鎖上notity喚醒。

也就是說,等待和喚醒必須是同一個鎖。

而鎖可以是任意對象,所以可以被任意對象調用的方法定義在Object類中。

優化後的代碼:

class Res {
	private String name;
	private String sex;
	boolean flag = false;
    //設置添加方法
	public synchronized void set(String name, String sex) {
		if (flag) {
			try {
				this.wait();//線程等待,沒有執行資格
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		this.name = name;
		this.sex = sex;
		flag = true;//有了數據,標識變爲true;
		this.notify();//喚醒線程池中的最早wait的線程
	}
     //輸出方法
	public synchronized void out() {
		if (!flag) {
			try {
				this.wait();//線程等待,沒有執行資格
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println(name + ".." + sex);//打印
		flag = false;//取走了數據,變爲false;
		this.notify();//喚醒線程池中的最早wait的線程
	}
}
//添加類,input
class Input implements Runnable {
	private Res r;

	public Input(Res r) {//關聯資源
		this.r = r;
	}
    //重寫run方法
	public void run() {
		// TODO Auto-generated method stub
		int x = 0;
		while (true) {

			if (x == 0) {
				r.set("mike", "man");
			} else {
				r.set("麗麗", "女");
			}
			x = (x + 1) % 2;

		}
	}

}
//輸出類
class Output implements Runnable {
	private Res r;

	public Output(Res r) {//關聯資源
		this.r = r;
	}

	public void run() {
		// TODO Auto-generated method stub
		while (true) {

			r.out();
		}
	}
}

public class Test {
	public static void main(String[] args) {
		Res r = new Res();//資源對象
		//創建兩個線程,並開啓
		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();

	}
}

五、Lock接口

解決線程安全問題使用同步的形式,(同步代碼塊,要麼同步函數)其實最終使用的都是鎖機制。

到了後期版本,直接將鎖封裝成了對象。線程進入同步就是具備了鎖,執行完,離開同步,就是釋放了鎖。

在後期對鎖的分析過程中,發現,獲取鎖,或者釋放鎖的動作應該是鎖這個事物更清楚。所以將這些動作定義在了鎖當中,並把鎖定義成對象。

所以同步是隱示的鎖操作,而Lock對象是顯示的鎖操作,它的出現就替代了同步。

在之前的版本中使用Object類中wait、notify、notifyAll的方式來完成的。那是因爲同步中的鎖是任意對象,所以操作鎖的等待喚醒的方法都定義在Object類中。

而現在鎖是指定對象Lock。所以查找等待喚醒機制方式需要通過Lock接口來完成。而Lock接口中並沒有直接操作等待喚醒的方法,而是將這些方式又單獨封裝到了一個對象中。

這個對象就是Condition,將Object中的三個方法進行單獨的封裝。並提供了功能一致的方法 await()、signal()、signalAll()體現新版本對象的好處。

< java.util.concurrent.locks > Condition接口:await()、signal()、signalAll();

成功的 lock 操作與成功的 Lock 操作具有同樣的內存同步效應。

成功的 unlock 操作與成功的 Unlock 操作具有同樣的內存同步效應


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章