黑馬程序員——java基礎——多線程



黑馬程序員——java基礎——多線程java

------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流! -------程序員

進程:是一個正在執行中的程序。每個進程執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元。
線程:就是進程中的一個獨立的控制單元。線程在控制着進程的執行。一個進程中至少有一個線程。
安全

一個進程至少有一個線程在運行,當一個進程中出現多個線程時,就稱這個應用程序是多線程應用程序,每一個線程在棧區中都有本身的執行空間,本身的方法區、本身的變量。多線程

jvm在啓動的時,首先有一個主線程,負責程序的執行,調用的是main函數。主線程執行的代碼都在main方法中。jvm

當產生垃圾時,收垃圾的動做,是不須要主線程來完成,由於這樣,會出現主線程中的代碼執行會中止,會去運行垃圾回收器代碼,效率較低,因此由單獨一個線程來負責垃圾回收。
擴展:其實更細節說明jvm,jvm啓動不止一個線程,還有負責垃圾回收機制的線程。

ide

隨機性的原理:由於cpu的快速切換形成,哪一個線程獲取到了cpu的執行權,哪一個線程就執行。函數


返回當前線程的名稱:Thread.currentThread().getName()性能

線程的名稱是由:Thread-編號定義的。編號從0開始。this

線程要運行的代碼都統一存放在了run方法中spa

線程要運行必需要經過類中指定的方法開啓。start方法。(啓動後,就多了一條執行路徑)

start方法:1)、啓動了線程;2)、讓jvm調用了run方法。

建立線程的方式

 建立線程共有兩種方式:繼承方式和實現方式

建立線程的第一種方式:繼承Thread,由子類複寫run方法。

步驟:

1,定義類繼承Thread類;

2,目的是複寫run方法,將要讓線程運行的代碼都存儲到run方法中;

3,經過建立Thread類的子類對象,建立線程對象;

4,調用線程的start方法,開啓線程,並執行run方法。

class MyThread extends Thread{
	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}
	public void run() {
		for(int i =0 ;i<10;i++){
			System.out.println("Demo:"+Thread.currentThread().getName()+":"+i);
		}
	}
}
public class ThreadDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread demo1 = new MyThread("線程一");
		demo1.start();
		MyThread demo2 = new MyThread("線程二");
		demo2.start();
		for(int i = 0;i<10;i++){
			System.out.println("main:"+i);
		}
		
	}
}

建立線程的第二種方式:實現一個接口Runnable

步驟:

1,定義類實現Runnable接口。

2,覆蓋接口中的run方法(用於封裝線程要運行的代碼)。

3,經過Thread類建立線程對象;

4,將實現了Runnable接口的子類對象做爲實際參數傳遞給Thread類中的構造函數。

爲何要傳遞呢?由於要讓線程對象明確要運行的run方法所屬的對象。

5,調用Thread對象的start方法。開啓線程,並運行Runnable接口子類中的run方法。

class Ticket implements Runnable{
	private  int tick = 100;
	public void run()
	{
		while(true)
		{
			if(tick>0)
			{
				System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
			}
		}
	}
}
class  TicketDemo
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();
		Thread t1 = new Thread(t);//建立了一個線程;
		Thread t2 = new Thread(t);//建立了一個線程;
		t1.start();
		t2.start();
	}
}

爲何要有Runnable接口的出現?

1:經過繼承Thread類的方式,能夠完成多線程的創建。可是這種方式有一個侷限性,若是一個類已經有了本身的父類,就不能夠繼承Thread類,由於java單繼承的侷限性。

但是該類中的還有部分代碼須要被多個線程同時執行。這時怎麼辦呢?

只有對該類進行額外的功能擴展,java就提供了一個接口Runnable。這個接口中定義了run方法,其實run方法的定義就是爲了存儲多線程要運行的代碼。

因此,一般建立線程都用第二種方式。

由於實現Runnable接口能夠避免單繼承的侷限性

2:實際上是將不一樣類中須要被多線程執行的代碼進行抽取。將多線程要運行的代碼的位置單獨定義到接口中。爲其餘類進行功能擴展提供了前提。

因此Thread類在描述線程時,內部定義的run方法,也來自於Runnable接口

實現Runnable接口能夠避免單繼承的侷限性。並且,繼承Thread,是能夠對Thread類中的方法,進行子類複寫的。可是不須要作這個複寫動做的話,只爲定義線程代碼存放位置,實現Runnable接口更方便一些。因此Runnable接口將線程要執行的任務封裝成了對象。

線程狀態:

被建立:start()

運行:具有執行資格,同時具有執行權;

凍結:sleep(time),wait()—notify()喚醒;線程釋放了執行權,同時釋放執行資格;

臨時阻塞狀態:線程具有cpu的執行資格,沒有cpu的執行權;

消亡:stop()


線程安全問題

多線程安全問題的緣由:
經過圖解:發現一個線程在執行多條語句時,並運算同一個數據時,在執行過程當中,其餘線程參與進來,並操做了這個數據。致使到了錯誤數據的產生。

涉及到兩個因素:
1,多個線程在操做共享數據。
2,有多條語句對共享數據進行運算。
緣由:這多條語句,在某一個時刻被一個線程執行時,尚未執行完,就被其餘線程執行了。
解決安全問題的原理:
只要將操做共享數據的語句在某一時段讓一個線程執行完,在執行過程當中,其餘線程不能進來執行就能夠解決這個問題。

在java中對於多線程的安全問題提供了專業的解決方式——synchronized(同步)

這裏也有兩種解決方式,一種是同步代碼塊,還有就是同步函數。都是利用關鍵字synchronized來實現。

a、同步代碼塊

用法:

        synchronized(對象)

         {須要被同步的代碼}

 同步能夠解決安全問題的根本緣由就在那個對象上。其中對象如同鎖。持有鎖的線程能夠在同步中執行。沒有持有鎖的線程即便獲取cpu的執行權,也進不去,由於沒有獲取鎖。

<span style="font-size:14px;">class Ticket implements Runnable
{
	private  int tick = 1000;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
				if(tick>0)
				{
					//try{Thread.sleep(10);}catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
				}
			}
		}
	}
}
class  TicketDemo2
{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		t2.start();
	}
}
</span>

b,同步函數

其實就是將同步關鍵字定義在函數上,讓函數具有了同步性。

同步函數是用的哪一個鎖呢?

經過驗證,函數都有本身所屬的對象this,因此同步函數所使用的鎖this

<span style="font-size:14px;">class Ticket implements Runnable
{
	private  int tick = 100;
	Object obj = new Object();
	boolean flag = true;
	public  void run()
	{
		if(flag)
		{
			while(true)
			{
				show();
			}
		}
	}
	public synchronized void show()//this
	{
		if(tick>0)
		{
			try{Thread.sleep(10);}catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
		}
	}
}
class  ThisLockDemo
{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		t1.start();
		
	}

</span>

當同步函數被static修飾時,這時的同步用的是哪一個鎖呢

靜態函數在加載時所屬於類,這時有可能尚未該類產生的對象,可是該類的字節碼文件加載進內存就已經被封裝成了對象,這個對象就是該類的字節碼文件對象。

因此靜態加載時,只有一個對象存在,那麼靜態同步函數就使用的這個對象。

這個對象就是類名.class

class Single
{
	private static Single s = null;
	private Single(){}
	public static  Single getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class)
			{
				if(s==null)
					//--->A;
					s = new Single();
			}
		}
		return s;
	}
}
class SingleDemo 
{
	public static void main(String[] args) 
	{
		System.out.println("Hello World!");
	}
}

同步代碼塊和同步函數的區別

同步代碼塊使用的鎖能夠是任意對象。

同步函數使用的鎖是this,靜態同步函數的鎖是該類的字節碼文件對象。

線程同步的利弊

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

弊端:相對下降性能,由於判斷鎖須要消耗資源,產生了死鎖。

定義同步是有前提的:

1,必需要有兩個或者兩個以上的線程,才須要同步。

2,多個線程必須保證使用的是同一個鎖。

同步死鎖

一般只要將同步進行嵌套,就能夠看到現象。同步函數中有同步代碼塊,同步代碼塊中還有同步函數。


public class DeadLockDemo {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
			// TODO Auto-generated method stub
			Ticket1 ticket1 = new Ticket1();
			Thread thread1 = new Thread(ticket1);
			thread1.start();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			Thread thread2 = new Thread(ticket1);
			ticket1.flag = false;
			thread2.start();
	}
}
class Ticket1 implements Runnable{

	private static int ticket1 = 10;
	private Object obj = new Object();
	boolean flag = true;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		if(flag){
			while(true){
				
				synchronized (obj) {
					show();
				}
			}
		}else{
			while(true){
				show();
			}
		}
	}
	public synchronized void show(){
		synchronized (obj) {
			if(ticket1>0){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+":"+ticket1--);
			}
		}
	}
}

線程間通信:
其實就是多個線程在操做同一個資源,可是操做的動做不一樣。

1:將資源封裝成對象。

2:將線程執行的任務(任務其實就是run方法。)也封裝成對象。

<span style="font-size:14px;">class Res
{
	String name;
	String sex;
	boolean flag = false;
}
class Input implements Runnable
{
	private Res r ;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			synchronized(r)
			{

				if(r.flag)
					try{r.wait();}catch(Exception e){}
				if(x==0)
				{
					r.name="mike";
					r.sex="man";
				}
				else
				{
					r.name="麗麗";
					r.sex = "女";
				}
				x = (x+1)%2;
				r.flag = true;
				r.notify();
			}
		}
	}
}
class Output implements Runnable
{
	private Res r ;
	
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{
				if(!r.flag)
					try{r.wait();}catch(Exception e){}
				System.out.println(r.name+"...."+r.sex);
				r.flag = false;
				r.notify();
			}
		}
	}
}
class  InputOutputDemo
{
	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();
	}
}
</span>

等待喚醒機制

涉及的方法:

wait:將同步中的線程處於凍結狀態。釋放了執行權,釋放了資格。同時將線程對象存儲到線程池中。

notify:喚醒線程池中某一個等待線程。

notifyAll:喚醒的是線程池中的全部線程

注意:

1:這些方法都須要定義在同步中。

2:由於這些方法必需要標示所屬的鎖。

   你要知道 A鎖上的線程被wait,那這個線程就至關於處於A鎖的線程池中,只能A鎖的notify喚醒。

3:這三個方法都定義在Object類中。爲何操做線程的方法定義在Object類中?

   由於這三個方法都須要定義同步內,並標示所屬的同步鎖,既然被鎖調用,而鎖又能夠是任意對象,那麼能被任意對象調用的方法必定定義在Object類中。

wait和sleep區別:

分析這兩個方法:從執行權和鎖上來分析:

wait:能夠指定時間也能夠不指定時間。不指定時間,只能由對應的notify或者notifyAll來喚醒。

sleep:必須指定時間,時間到自動從凍結狀態轉成運行狀態(臨時阻塞狀態)

wait:線程會釋放執行權,並且線程會釋放鎖。

Sleep:線程會釋放執行權,可是不釋放鎖。















相關文章
相關標籤/搜索