Java多線程,線程安全,線程之間通信,以及線程池詳解

多線程

1.認識多線程
瞭解併發/並行
併發:指兩個或多個事件在同一個時間段內發生(交替執行)
並行:指兩個或多個事件在同一時刻發生(同時執行)
進程:
進程:指一個內存中運行的應用程序,每個進程都有一個獨立的空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;
系統運行一個程序即是一個進程從創建 運行 到消亡的過程

線程:
線程是進程中的一個執行單元,負責當前進程的執行,一個進程中至少有一個線程,一個進程中是可以有多個線程的。這個應用程序也可以稱爲多線程程序
簡而言之:一個程序運行後至少一個進程,一個進程中可以包含多個線程(線程是進程中的一個執行單元,負責程序中的執行)
多線程的好處:1.效率高 2.多個線程之間互不影響

線程的調度:
1.分時調度
所有線程輪流使用CPU的使用權,平均分配每個線程佔用CPU的時間
2.搶佔式調度
優先讓優先級高的線程使用CPU ,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性) Java使用的爲搶佔式調度

主線程: 執行主(main)方法的線程
Jvm執行main方法,main方法進入到棧內存中,Jvm會找操作系統開闢一條main方法通向CPU的執行路徑 CPU就可以通過這個路徑來執行main方法,這個路徑就是main(主)線程
單線程:java程序中只有一個線程 執行從main方法開始,從上往下依次執行

多線程原理
隨機性打印結果原因
在這裏插入圖片描述
多線程內存圖解
在這裏插入圖片描述

Java.lang.Thread類 常用方法
構造方法:
public Thread();分配一個新的線程對象
public Thread(String name); 分配一個指定名字的線程對象
public Thread(Runnable traget,String name);
分配一個帶有指定目標新的線程對象並指定名字

常用方法:
1.獲取當前線程的名稱:

方法1:使用Thread類中的方法 getName()
1.public String getName() 返回當前線程名稱

獲取線程的名字 方法1
String name = getName();
System.out.println(name);

方法2::可以先獲取到當前正在執行的線程,使用線程中的方法getName() 獲取當前正在執行的線程的信息

2.public static Thread currentThread();返回當前正在執行的線程對象的引用
線程的名字:主線程:main 新線程:Thread-0,Thread-1,Thread-2

Thread name = Thread.currentThread();
System.out.println(name);//Thread[Thread-0,5,main]     5:線程的優先級 
String string = name.getName();
System.out.println(string)

等價於:(鏈式編程)System.out.println(Thread.currentThread().getName());

2.設置線程的名稱(瞭解)

1.public void setName(String name) 將此線程的名稱更改爲等於參數 name
2.創建一個帶參數的構造方法,參數傳遞線程的名稱,調用父類的帶參構造方法,把線程名稱傳遞給父類,讓父類Thread 給子線程起一個名字
Thread(String name) 分配一個新的 Thread對象。

3.線程的休眠
public static void sleep(long millis) 使當前正在執行的線程以指定的毫秒暫停
毫秒數結束以後,線程繼續執行

創建線程類

java使用java.lang.Thread 類代表線程 所有的線程對象都必須是Thread類或子類的實例

創建多線程的第一種方式 :
創建Thread類的子類
java.lang.Thread 是描述線程的類 ,我們想要實現多線程程序,就必須繼承Thread類
實現步驟:
1.創建一個Thread類的子類,
2.在Thread類的子類中的run方法 設置線程任務(開啓線程做什麼)
3.創建一個Thread類的子類對象
4.調用Thread類中的方法 start方法 開啓新的線程 執行run方法

ps:void start() 使該線程開始執行,Java虛擬機調用線程的run方法
結果是兩個線程併發的運行:當前線程(main線程)和另一個線程(創建的新線程,執行其run方法)。
多次啓動一個線程是非法的,特別是當前線程已經結束執行後,不能再重新啓動

注意:java程序屬於搶佔式調度,那個線程的優先級高,那個線程優先執行;同一優先級隨機選擇一個線程執行

public class MyThread extends Thread{
	//1.創建一個Thread類的子類
	//2.在Thread類的子類中的run方法  設置線程任務(開啓線程做什麼)
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("run"+i);
		}
	}
}


public class Demo03MuitCode {
	public static void main(String[] args) {
		//3.創建一個Thread類的子類對象
		MyThread p1 = new MyThread();
		//4.調用Thread類中的方法  start方法 開啓新的線程 執行run方法
		p1.start();
		
		for (int i = 0; i < 20; i++) {
			System.out.println("main"+i);
		}
	}
}

創建多線程的第二種方式:
實現java.lang.Runnable接口 也是非常常見的一種創建線程的方式。我們只需要重寫run方法即可
Runnable接口應由那些打算通過某一線程執行其 實例的類來實現。 該類必須定義一個無參數的方法,稱爲run 。
java.lang.Thread的構造方法:

Thread(Runnable target) 分配一個新的 Thread對象。  
Thread(Runnable target, String name) 分配一個新的 Thread對象。

實現步驟:
1.創建一個Runable接口的實現類
2.在實現類中重寫Runable接口的run方法。設置線程任務
3.創建一個Runable接口的實現類對象
4.創建Thread類對象,構造方法中傳遞Runable接口的實現類對象
5.調用Thread類中的start方法,開啓新的線程執行run方法

public class Runableimpl implements Runnable{
	//1.創建一個Runable接口的實現類
	@Override
	public void run() {
	//2.在實現類中重寫Runable接口的run方法。設置線程任務
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

public class Demo06 {
	public static void main(String[] args) {
		//3.創建一個Runable接口的實現類對象
		Runableimpl impl = new Runableimpl();
		//4.創建Thread類對象,構造方法中傳遞Runable接口的實現類對象
		Thread thread = new Thread(impl);
		//5.調用Thread類中的start方法,開啓新的線程執行run方法
		thread.start();
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

使用Runable接口創建多線程程序的好處

如果一個類繼承Thread ,則不適合資源共享,但是如果實現了Runable接口的話 就很容易實現資源共享

實現Runable接口比繼承Thread類所有的優勢
1.適合多個相同的程序代碼的線程去共享同一個資源
2.可以避免java中的單繼承的侷限性
一個類只能繼承一個類(一個人只能由一個親爹) 類繼承了Thread類就不能繼承其他類,而實現了Runable接口,還可以繼承其他的類,實現其他的接口
3.增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立
實現Runable接口的方式,把設置線程任務和開啓新線程進行了分離(解耦)
4.線程池只能放入實現Runable或者Callable類線程。不能直接放入繼承Thread的類

擴展
在Java中,每次程序運行至少啓動2個線程,一個是main線程,一個是垃圾回收線程,視爲每當使用java命令執行一個類的時候,實際上都會啓動一個jvm每個jvm其實就是在操作系統中啓動了一個進程

匿名內部類的方式實現線程的創建

回顧
匿名:沒有名字
內部類:寫在其他類內部的類
匿名內部類的作用:簡化代碼

實現原理
把子類繼承父類,重寫父類的方法,創建子類對象合成一步完成
把實現類實現接口,重寫接口中的方法,創建實現類對象合成一步完成

匿名內部類的最終產物:子類/實現類對象,而這個類沒有名字

格式:
new 父類/接口(){
重寫父類/接口中的方法;
}
//1.線程的父類Thread方法

//1.線程的父類Thread方法
		new Thread() {
			//2.重寫run方法
			@Override
			public void run() {
			for (int i = 0; i < 20; i++) {
				System.out.println(Thread.currentThread().getName()+"->"+"線程1");
			}
			}
		}.start();

//2.線程接口的方式

Runnable runnable = new Runnable() { //採用匿名的方法實現的接口 賦值給一個Runable
	@Override
	//重寫run方法,設置線程任務
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+"->"+"線程2");
				}
			}
		};
new Thread(runnable).start();

//簡化線程接口的方法

new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"->"+“線程3”);
}
}
}).start();

多線程安全問題

買票問題 多個線程同時買一張票的問題

public class Runableimpl implements Runnable{
	//定義一個多個線程共享的票源
	private int ticker = 100;
	@Override
	//設置線程任務,賣票
	public void run() {
		//使用死循環,讓賣票重複操作
		while(true) {
			if (ticker > 0) {
				//提高安全問題出現的概率,讓城鄉睡眠一下
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"正在賣第:"+ticker+"張票");
				ticker--;
			}else {
				break;
			}
		}
	}
}

解決線程安全問題: 線程同步機制
1.同步代碼塊
格式

	sychranized(鎖對象){
	可能會出現線程安全問題的代碼(訪問了共享數據的代碼)
}

注意:
1.通過代碼塊中的鎖對象,可以使用任意的對象
2.但是必須保證多個線程使用的鎖對象是同一個
3.鎖對象作用:
把同步代碼塊鎖住,只讓線程在同步代碼塊中執行
同步技術的原理
使用了一個鎖對象,這個鎖對象叫同步鎖,也叫對象鎖,也叫對象監視器
同步中的線程,沒有執行完畢不會釋放當前鎖。同步外的線程沒有鎖進不去同步
同步保證了只能有一個線程在同步中執行共享數據,保證了安全,但程序會頻繁的獲取鎖,釋放鎖,程序的效率會有所降低

2.同步方法
格式:

修飾符 synchronized 返回值類型  方法名(參數列表){
	//訪問了共享數據的代碼
}

實現步驟:
1.把訪問了共享數據的代碼抽取出來,放到一個方法中
2.在方法上添加synchronized 修飾符
同步方法鎖住的就是實現類的對象 new Runable(); 也就是this
靜態的同步方法:(瞭解)
靜態方法的參數只能調用靜態的
靜態的同步方法的鎖對象是?
不能是this,this是創建對象之後產生的,靜態方法優先於對象,靜態方法的鎖對象是本類的class屬性 -》class文件對象(反射)

public static synchronized void staticmethod() {
		if(ticker >0) {  //靜態方法的參數只能調用靜態的
			System.out.println(Thread.currentThread().getName()+"正在賣第:"+ticker+"張票");
			ticker--;
		}
	}

3.鎖機制
Lock鎖
Jdk1.5後提供了 Java.util.concurrent.locks.Lock 提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作
同步代碼塊/同步方法 具有的功能Lock都有,除此之外更強大,更體現面向對象
lock接口中的兩個方法:
1.void lock() 獲得鎖。
2.void unlock() 釋放鎖。
java.util.concurrent.locks.ReentrantLock 實現了lock接口
使用步驟:
1.在成員位置創建一個ReentrantLock對象
2.在可能會出現線程安全問題 前 調用Lock接口中的方法 lock獲取鎖
3.在可能會出現線程安全問題 後 調用Lock接口中的方法 unlock釋放鎖
我們可以把釋放鎖放在 finally代碼塊中 無論程序是否是否異常,都會把鎖釋放。提高程序的效率

public class Runableimpl3 implements Runnable{
	//定義一個多個線程共享的票源
	private int ticker = 100;
	//創建一個Lock鎖實現對象
	ReentrantLock lock = new ReentrantLock();
	@Override
	public void run() {
		while(true) {
			lock.lock();
			try {
				//在可能發生線程安全問題前 調用Lock接口中的方法  lock獲取鎖
				if (ticker > 0) {
					System.out.println(Thread.currentThread().getName()+"正在賣第:"+ticker+"張票");
					ticker--;
				}else {
					break;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				//在可能發生線程安全問題後 調用Lock接口中的方法  unlock釋放鎖
				lock.unlock(); //無論程序是否是否異常,都會把鎖釋放。提高程序的效率
			}
		}
	}
}

線程的狀態

線程的狀態,線程可以處以下列狀態之一
NEW 尚未啓動的線程處於此狀態。
RUNNABLE 在Java虛擬機中執行的線程處於此狀態。
BLOCKED 被阻塞等待監視器鎖定的線程處於此狀態。
WAITING 正在等待另一個線程執行特定動作的線程處於此狀態。
TIMED_WAITING 正在等待另一個線程執行動作達到指定等待時間的線程處於此狀態。
TERMINATED 已退出的線程處於此狀態

進入到TIMED_WAITING(記時等待) 有兩種方式:
1.使用sleeep(long time)方法 在毫秒值之後。線程睡醒進入到Runnable/Blocked狀態
2.使用wait(long time)方法,如果在毫秒值之後,還沒有被notify喚醒,就會自動醒來,進入到Runnable/Blocked狀態
喚醒的方法:
void notify() 喚醒監視器上的單個線程
void notifyAll() 喚醒監視器上的所有線程

public class Demo04 {
	public static void main(String[] args) {
		//創建一個鎖對象,保證鎖對象唯一
				Object obj = new Object();
				//創建一個顧客線程(消費者)
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//保證等待和喚醒,只能由一個在執行,需要使用同步技術
							synchronized (obj) {
								System.out.println("顧客1告知老闆要的包子的種類和數量");
								try {
									obj.wait(5000);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								//喚醒之後執行的代碼
								System.out.println("包子拿到了,顧客1開吃");
							}
						}
					}
				}.start();
				//創建一個顧客2線程(消費者)
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//保證等待和喚醒,只能由一個在執行,需要使用同步技術
							synchronized (obj) {
								System.out.println("顧客二告知老闆要的包子的種類和數量");
								try {
									obj.wait(5000);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								//喚醒之後執行的代碼
								System.out.println("包子拿到了,顧客2開吃");
							}
						}
					}
				}.start();	
				
				//創建一個老闆線程(生成者):
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//花5秒鐘做包子
							try {
								sleep(5000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							//保證等待和喚醒,只能由一個在執行,需要使用同步技術
							synchronized (obj) {
								System.out.println("包子做好了,請慢用");
								obj.notifyAll();
							}
						}
					}
				}.start();
	}
}

線程之間的通信

等待喚醒機制
多個線程在處理同一個資源,但是處理的動作(線程和任務)卻不同
多個線程在處理統一資源,並且任務不同,多個線程在操作同一份數據時,避免對統一共享變量的爭奪,我們需要通過一定的手段使各個線程能夠有效的利用資源
等待喚醒機制: 多個線程間的一種 協作 機制

等待喚醒案例
創建一個顧客線程(消費者):告知老闆要的包子的種類和數量。調用wait方法,放棄cpu的執行,進入WAITING狀態(無限等待狀態)
創建一個老闆線程(生成者):花了5S做這個包子,做好包子後,調用notify方法,喚醒顧客吃包子
注意:
顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒,只能由一個在執行
同步使用的鎖對象必須保證唯一
//創建一個鎖對象,保證鎖對象唯一
Object obj = new Object();
只有鎖狀態才能調用wait和notify方法
Object類中的方法:
void wait() 導致當前線程等待,直到另一個線程調用該對象的 notify()方法或 notifyAll()方法。
void notify() 喚醒正在等待對象監視器的單個線程。 會繼續執行wait方法之後的方法

public class ThreadState {
	public static void main(String[] args) {
		//創建一個鎖對象,保證鎖對象唯一
		Object obj = new Object();
		//創建一個顧客線程(消費者)
		new Thread() {
			@Override
			public void run() {
				while(true) {
					//保證等待和喚醒,只能由一個在執行,需要使用同步技術
					synchronized (obj) {
						System.out.println("告知老闆要的包子的種類和數量");
						try {
							obj.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						//喚醒之後執行的代碼
						System.out.println("包子拿到了,開吃");
					}
				}
			}
		}.start();
		
		//創建一個老闆線程(生成者):
		new Thread() {
			@Override
			public void run() {
				while(true) {
					//花5秒鐘做包子
					try {
						sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					//保證等待和喚醒,只能由一個在執行,需要使用同步技術
					synchronized (obj) {
						System.out.println("包子做好了,請慢用");
						obj.notify();
					}
				}
			}
		}.start();
	}
}

線程池的概念及原理

線程池:容器 -》 集合(ArrayList,HashSet 一般採用LinkedList , HashMap)
**線程池:**其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源
1.當程序第一次啓動的時候,創建多個線程,保存到一個集合中
2.當我們想要使用線程的時候,就可以從集合中取出來線程使用
3. 如果是List集合 我們可以使用
Thread t = list.remove(0) 返回被移出的元素(線程只需要被一個任務使用)
如果是linked集合 我們可以使用
Thread 他= lisked.removedFirst();
4.當我們使用完畢線程,需要把線程歸還給線程池
list.add(t);
linked.addLast(t):

在Jdk1.5 以後 就內置了線程池 我們可以直接使用

java.util.concurrent.Executors :線程池的工廠類,用來生成線程池
Executors 類中的靜態方法
static ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定線程數量的線程池
參數:int nThreads : 創建線程池中,包含的線程數量
返回值:ExecutorService 返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)

java.util.concurrent.ExecutorService:線程池接口
用來從線程池中獲取線程,調用start方法,執行線程任務
submit(Runnable task) 提交一個Runnable 任務用於執行
(瞭解)關閉/銷燬線程池的方法
void shutdown() 啓動有序關閉,其中先前提交的任務將被執行,

線程池的使用步驟:
1.使用線程池的工廠類Executors裏面提供的靜態方法:newFixedThreadPool(int nThreads)生產一個值得線程數量的線程池
2.創建一個類,實現Runnable接口 ,重寫run方法 設置線程任務
3.調用ExecutorService中的方法 submit 傳遞線程任務(實現類) 開啓線程,執行run方法
4.調用ExecutorService中的方法 shutdown 銷燬線程池(不建議執行)

public class ThreadPool {
	public static void main(String[] args) {
		//1.使用線程池的工廠類Executors裏面提供的靜態方法:newFixedThreadPool(int nThreads)生產一個值得線程數量的線程池
		//獲取一個線程池
		ExecutorService service =  Executors.newFixedThreadPool(2);
		//3.調用ExecutorService中的方法 submit 傳遞線程任務(實現類)  開啓線程,執行run方法
		service.submit(new Runableimpl4());//pool-1-thread-1創建了一個新的線程
		//線程池會一直開啓,使用完了線程,會自動把線程歸還給線程池,線程可以繼續使用
		service.submit(new Runableimpl4());//pool-1-thread-1創建了一個新的線程
		service.submit(new Runableimpl4());//pool-1-thread-2創建了一個新的線程
		//4.調用ExecutorService中的方法  shutdown 銷燬線程池(不建議執行)
		service.shutdown(); //關閉線程池
	}
}

//2.創建一個類,實現Runnable接口,重寫run方法,設置線程任務
public class Runableimpl4 implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"創建了一個新的線程");
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章