多線程
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()+"創建了一個新的線程");
}
}