一、多線程簡介
(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等錯票
(2)、解決辦法:1、問題原因:
當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。
導致共享數據的錯我。
對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。
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 操作具有同樣的內存同步效應