Java自學第8期——多執行緒
1、多執行緒:
作業系統支援同時執行多個任務,一個任務通常是一個程式,所有執行中的程式就是一個程序()。程式內部包含多個順序執行流,每個順序執行流就是一個執行緒。
併發:兩個或者多個事件在同一個時間段內交替發生;
並行:兩個或者多個事件在同一時刻同時發生;
某時間段內巨集觀上有多個程式同時執行,在單cpu系統中,每一時刻只有單個程式在執行,但在微觀上cpu是交替執行他們的。
多cpu系統中,這些多工被分配到多個cpu上,這樣多個任務就是同時執行。
1.1 執行緒與程序
程序:程序是併發執行程式在執行過程中資源分配和管理的基本單位(資源分配的最小單位)。程序可以理解為一個應用程式的執行過程,
應用程式一旦執行,就是一個程序。每個程序都有自己獨立的地址空間,每啟動一個程序,
系統就會為它分配地址空間,建立資料表來維護程式碼段、堆疊段和資料段。
執行緒:程式執行的最小單位。一個程序至少有一個執行緒,擁有多執行緒的應用程式被稱為多執行緒程式。
1.2 執行緒排程
分時排程:所有執行緒輪流使用cpu,平均分配每個orffff執行緒佔用cpu的時間
搶佔式排程:優先讓優先順序高的執行緒使用cpu,優先順序相同則隨機選擇。java使用搶佔式排程。
設定執行緒優先順序:工作管理員開啟詳細資訊,右鍵設定執行緒優先順序
1.3 建立執行緒類
java.lang.Thread類代表執行緒,所有執行緒類都必須是Thread類或者是其子類的例項,每個執行緒的作用是
完成一定的任務,即是執行一段程式流即一段順序執行的程式碼,java使用執行緒執行體來代表這段程式流。
通過繼承Thread類來建立並啟動多執行緒:
1.定義Thread的子類,重寫run()方法,其方法體代表執行緒執行的任務,該run()方法叫執行緒執行體、
2.建立子類的物件
3.呼叫執行緒物件的start()方法來啟動該執行緒
//定義測試類
public class Demo01 {
public static void main(String[] args){
Demo02 thread1 = new Demo02("執行緒1");
thread1.run();
}
}
//定義自定義執行緒類 class Demo02 extends Thread{ //定義指定執行緒名稱的構造方法 public Demo02(String name){ //呼叫父類的String引數的構造方法,指定執行緒的名稱 super(name); } @Override public void run() { for (int i = 0; i <10 ; i++) { System.out.println(getName()+" is runnimg "+i); } } }
2、瞭解Thread類
java.lang.Thread
構造方法:
public Thread() :分配一個新的執行緒物件。
public Thread(String name) :分配一個指定名字的新的執行緒物件。
public Thread(Runnable target) :分配一個帶有指定目標新的執行緒物件。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的執行緒物件並指定名字。
常用方法:
public String getName() :獲取當前執行緒名稱。
public void start() :導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法。
public void run() :此執行緒要執行的任務在此處定義程式碼。
public static void sleep(long millis) :使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行)。
public static Thread currentThread() :返回對當前正在執行的執行緒物件的引用。
2.1 建立執行緒的方式有兩種:
一種是繼承Thread類,另一種是實現Runnable介面方式
推薦實現Runnable介面:
1.定義該介面的實現類,並重寫該介面的run()方法,即為執行緒的執行體
2.建立該實現類的例項,並以該例作為Thread的target來建立Thread執行緒物件,該物件為真正的執行緒物件
3.呼叫執行緒物件的start()方法啟動執行緒
2.2 Thread類和Runnable類的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。
總結:
實現Runnable介面比繼承Thread類所具有的優勢:
- 適合多個相同的程式程式碼的執行緒去共享同一個資源。
- 可以避免java中的單繼承的侷限性。
- 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。
- 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類。
擴充:在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用
java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個進
程。
public class Demo04 implements Runnable {//定義實現類
//重寫run方法
@Override
public void run() {
for (int i = 0; i <3 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
class test1 {
public static void main(String[] args) {
//建立實現類物件
Demo04 obj = new Demo04();
//將該實現類物件給建立的執行緒物件,並賦名。參考Thread類的構造方法
Thread thread1 = new Thread(obj,"執行緒");
thread1.start();
}
}
2.3匿名內部類方式實現執行緒的建立
使用該方式,可以方便的實現每個執行緒執行不同的任務操作
使用該方式實現Runnable介面:
public class Demo05 {
public static void main(String[] args) {
// new Runnable(){
// @Override
// public void run() {
// for (int i = 0; i < 3; i++) {
// System.out.println(Thread.currentThread().getName()+i);
// }
// }
// };
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(r).start();
for (int i = 0; i <4 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
後面學習使用lambda表示式優化寫法。
3、執行緒安全問題
執行緒安全:多個執行緒同時執行,執行同一段程式碼時,如果程式每次執行結果和單執行緒執行的結果是一樣的,同時其他變數的值和預期也是一樣。
3.1 執行緒同步
要解決多執行緒併發訪問同一資源可能出現的安全問題。可通過同步機制來解決(synchronized)
要求在某個執行緒修改共享資源的時候,其他執行緒不能修改該資源,等待修改完畢且同步後,
才能去搶奪cpu資源,完成對應的操作,保證了資料的同步性,從而保證執行緒安全。
有三種方法完成同步操作:
1、同步程式碼塊
2、同步方法
3、鎖機制
同步程式碼塊:
synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問
執行緒開始執行同步程式碼塊之前,必須先獲得同步監視器的鎖定。任何時刻只能有一個執行緒可以獲得對同步監視器的鎖定,當同步程式碼塊執行完成後,該執行緒會釋放對該同步監視器的鎖定。
格式:
synchronized(同步鎖/同步監視器){
需要同步操作的程式碼
}
同步鎖:
理解為給物件上鎖,
1、鎖物件,可以是任意型別
2、多個執行緒物件,要使用同一把鎖
注意:任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他執行緒只能在外等著。
public class Demo07 implements Runnable{
private int num = 10;
//建立lock
Object lock = new Object();
@Override
public void run() {
while(true) {//一直執行
//新增同步鎖
synchronized(lock) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "數字為:" + num--);
}
}
}
}
}
class test2{
public static void main(String[] args) {
Demo07 x = new Demo07();
Thread a = new Thread(x,"執行緒a");
Thread b = new Thread(x,"執行緒b");
Thread c = new Thread(x,"執行緒c");
a.start();
b.start();
b.start();
}
}
此時列印的內容已經沒有重複的出現了,已經執行緒安全
同步方法:
概念:使用synchronized修飾的方法,叫做同步方法,保證a執行緒執行該方法的時候,其他方法只能在外面等著。
格式:
public synchronized void method(){
可能會產生執行緒安全問題的程式碼
}
同步鎖,
對於非static方法,同步鎖就是this,
對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)
public class Demo08 implements Runnable {
private int num = 10;
@Override
public void run() {
while(true){//一直執行
method1();
}
}
//同步方法
public synchronized void method1(){
if (num>0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
//獲取當前執行緒的名字
String name = Thread.currentThread().getName();
System.out.println(name+"數字為:"+num--);
}
}
}
3.2 Lock鎖
java.util.concurrent.locks.Lock機制提供了比synchronized程式碼塊和synchronized方法更加廣泛的鎖定操作,同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向物件。
Lock鎖又稱同步鎖,加鎖與釋放鎖方法化了,如下:
public void lock():加同步鎖。
public void unlock():釋放同步鎖。
public class Demo09 implements Runnable {
private int num = 10;
//建立lock
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {//一直執行
//新增同步鎖
lock.lock();
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//釋放同步鎖,finally確保一定會執行
lock.unlock();
}
String name = Thread.currentThread().getName();
System.out.println(name + "數字為:" + num--);
}
}
}
}
class test5{
public static void main(String[] args) {
Demo09 obj = new Demo09();
Thread thread = new Thread(obj,"執行緒x");
thread.start();
}
}
4、執行緒狀態
當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中,有幾種狀態呢?在API中java.lang.Thread.State 這個列舉中給出了六種執行緒狀態:
Object中兩個方法 wait()和notify()方法,
1.NEW(新建) :
執行緒剛被建立,但是並未啟動。還沒呼叫start方法。
2.Runnable(可執行):
執行緒可以在java虛擬機器中執行的狀態,可能正在執行自己程式碼,也可能沒有,這取決於操
作系統處理器。
3.Blocked(鎖阻塞):
當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀
態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態。
4.Waiting(無限等待):
一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個
狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。
5.Timed Waiting(計時等待):
同waiting狀態,有幾個方法有超時引數,呼叫他們將進入Timed Waiting狀態。這一狀態
將一直保持到超時期滿或者接收到喚醒通知。帶有超時引數的常用方法有Thread.sleep 、
Object.wait。
6.Teminated(被終止):
因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。
4.1 Timed Waiting(計時等待)
一個正在限時等待另一個執行緒執行一個(喚醒)動作的執行緒處於這一狀態。在run方法中新增sleep語句,就強制當前正在執行的執行緒休眠(暫停執行),以減慢執行緒。
當我們呼叫了sleep方法之後,當前正在執行是執行緒進入休眠狀態。
1.呼叫sleep方法,單獨的執行緒可以呼叫,不一定非要有協作關係
2.為了讓其他執行緒有機會執行,可以將Thread.sleep()呼叫放線上程run()之內。以保證
該執行緒執行過程中會睡眠。
3.sleep與鎖無關,執行緒睡眠到期自動甦醒,並返回到Runnable(可執行狀態)
4.sleep中指定的時間是執行緒暫停執行的最短時間,不能保證時間到期後立即就會執行
4.2 BLOCKED(鎖阻塞):
一個正在阻塞等待一個監視器鎖(鎖物件)的執行緒處於這一狀態。
執行緒a和執行緒b使用同一個鎖,如果執行緒a獲取到鎖,則進入Runnable狀態,那麼執行緒b進入
到Blocked鎖阻塞狀態(即b沒有爭取到鎖物件)。
public class Demo10 extends Thread {
//實現一個計數器,計數到100,
// 在每個數字之間暫停1秒,每隔10個數字輸出一個字串
public void run(){
for (int i = 0;i < 100;i++){
if ((i) % 10 == 0){
System.out.println("——————" + i);
}
System.out.println(i);
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
new Demo10().start();
}
}
4.3 Waiting(無限等待)
一個正在無限期等待另一個執行緒執行一個特別的(喚醒)動作的執行緒處於這一狀態。
Onject中的兩個方法:
void wait():在其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法前,讓當前執行緒等待。
void notify():喚醒此物件監視器上等待的單執行緒,繼續執行wait之後的程式碼
void notify():喚醒所有正在等待的執行緒
public class Demo11 {
//建立鎖物件,保證唯一
public static Object obj = new Object();
public static void main(String[] args) {
//演示Waiting
//
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//新增同步鎖,保證只有一個執行緒執行
synchronized (obj) {
try {
System.out.println(Thread.currentThread().getName() + "獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件");
obj.wait();//無限等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===從waiting狀態醒來,獲取到鎖物件,繼續執行了");
}
}
}
}, "等待執行緒").start();
new Thread(new Runnable() {
@Override
public void run() {
// while(true){
try {
System.out.println(Thread.currentThread().getName() + "——等待3秒鐘");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "獲取到時鎖物件,呼叫notify方法。釋放鎖物件");
obj.notify();
}
}
}, "喚醒執行緒").start();
}
}
5、執行緒池:
如果避免頻繁啟動和銷燬執行緒(尤其是大量生命短的執行緒),造成系統性能下降,使用執行緒池的方法。
執行緒池建立大量空閒執行緒,當執行緒池接收建立的Runnable物件,便會將一個空閒的執行緒執行該物件的run()方法,執行結束後迴歸空閒狀態,暫時不進行銷燬。
執行緒池還能控制併發執行緒的數量,通過他的最大執行緒數目的引數。
java5後Executors工廠類生產執行緒池,靜態方法:
newFixedThreadPool(int nThreads):建立一個可重用的、具有固定執行緒數的執行緒池.
newScheduledThreadPool(int corePoolSize):建立一個執行緒池,指定延遲後執行。
java8新增:
newWorkStealingPool(in parallelism):
建立持有足夠的執行緒池來支援給定的並行級別,該方法還會使用多個佇列減少競爭,可以無引數,自動匹配當前可支援最高並行級別,ExecutorService代表儘快執行的執行緒池,提交Runnable物件則會盡快執行該任務。靜態方法:
Future<?> submit(Runnable task):
提交Runnable物件,執行緒池將在有空閒時執行該任務。run()執行完後返回null。
提交給一個Runnable物件給執行緒池,空閒時執行。run()方法結束後返回result。
用完後,呼叫該執行緒池的shutdown()方法,變不再接受新的任務,但會將之前提交的任務完成。全部完成後,池中所有執行緒會死亡。
呼叫shutdownNow()方法,則立刻停止所有正在執行的活動任務,暫停正在等待的任務,並返回等待執行的任務的列表。
總步驟:
1、呼叫Executors類的靜態工廠方法建立一個ExecutorService物件,該物件代表一個執行緒池。
2、建立Runnable實現類,作為執行緒執行任務。
3、呼叫ExecutorService物件的submit()方法來提交Runnable例項。
4、當不想提交任何任務時,呼叫ExecutorService物件的submit()方法來結束執行緒池。
public class Demo12_ThreadPool {
public static void main(String[] args) throws Exception{
//建立一個固定6個執行緒的執行緒池
ExecutorService pool = Executors.newFixedThreadPool(6);
Runnable obj1 = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執行");
}
};
Runnable obj2 = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執行");
}
};
pool.submit(obj1);
pool.submit(obj2);
//關閉執行緒池
pool.shutdown();
}
}
下一期記錄lambda表示式的使用方法。