線程的概念:
一個線程是指程序中完成一個任務的執行流,java中可以在一個程序中併發地運行多個線程,這些線程可以同時在多個處理器上運行
在單CPU系統中,多個線程分享CPU的時間,操作系統負責CPU資源的調度和分配
多線程可以使程序的反應更快,交互性更強,執行效率更高。當程序作爲一個應用程序(application)運行時,jvm會爲main方法創建一個線程,當程序不再需要時,jvm就會創建一個線程來進行垃圾回收的工作。所以一個完整的應用程序最少含有兩個線程。
2. 創建線程
2.1 繼承父類Thread
- 將類聲明爲Thread的子類
- 重寫Thread類的run()方法
- 創建Thread的子類對象,並且調用start()方法,啓動線程
public class MyThread extends Thread {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for(int i=0;i<100;i++){
System.out.println("main-->"+i);
}
}
public void run() {
for(int i=0;i<100;i++){
System.out.println("自定義-->"+i);
}
}
}
2.2 實現Runnable接口
- 創建類並實現Runnable接口
- 實現Runnable接口裏的run方法,完成自定義線程的任務代碼
- 實例化類
- 創建Thread對象,參考Thread構造方法:Thread(Runnable target)
- 調用start()方法,啓動線程
public class MyThread3 implements Runnable {
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread()+":"+i);
}
}
public static void main(String[] args) {
//創建類實例
MyThread3 thread3 = new MyThread3();
//創建Thread類的實例,把Runnable作爲實參傳遞
Thread thread = new Thread(thread3,"線程-C");
//調用thread的start()方法啓動線程
thread.start();
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread()+":"+i);
}
}
}
Note:因爲java是單繼承、多實現的,所以建議使用實現Runnable接口的方法來實現線程
3. 線程常見的方法
- 設置線程的名字
可以使用帶參數的構造方法Thread(String name) 或者thread.setName(“myThread”)方法,爲線程命名。同樣getName()可以得到線程的名字。 - 睡眠線程
注意:sleep是靜態方法,所以哪一個線程執行了含有sleep方法的代碼,哪一個線程就會睡眠
考慮以下代碼是哪一個線程睡眠了?
public class MyThread2 extends Thread {
//自定義線程
public void run() {
for(int i=0;i<100;i++){
System.out.println(i);
}
}
//主線程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2();
thread2.sleep(5 * 1000);
thread2.setName("線程-A");
thread2.start();
}
}
“thread2.sleep(5 * 1000);”是在主線程中執行,所以主線程會被睡眠
public class MyThread2 extends Thread {
//自定義線程
public void run() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<100;i++){
System.out.println(i);
}
}
//主線程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
“Thread.sleep(2 * 1000);”是在自定義線程中執行,所以自定義線程會被睡眠
3. 得到當前線程對象
靜態方法,哪個線程執行了含有currentThread()的代碼,就返回哪個線程的對象
public class MyThread2 extends Thread {
public MyThread2(String name) {
super(name);
}
//自定義線程
public void run() {
System.out.println("this:"+this);
System.out.println("2:"+Thread.currentThread());
}
//主線程
public static void main(String[] args) throws InterruptedException {
MyThread2 thread2 = new MyThread2("線程-B");
thread2.start();
System.out.println("1:"+thread2.currentThread());
}
}
運行後控制檯輸出如下:
4. 設置線程的優先級
1:Thread[main,5,main]
this:Thread[線程-B,5,main]
2:Thread[線程-B,5,main]
優先級越高的線程獲取CPU資源的機率越大
//設置優先級1~10之間,數值越大優先級越高,默認的優先級爲5
thread2.setPriority(10);
優先級越高的線程不會100%優先執行,只是優先執行的概率更大而已
5. isAlive()
isAlive()是用來判斷線程的狀態,當線程處於就緒、臨時阻塞、運行狀態,則返回true;如果線程處於新建並且沒有啓動的狀態,或者線程已經執行結束,則返回false.
4. 線程的生命週期
- 新建狀態:new Thread()後,線程就進入了新建狀態
- 調用start()方法啓動線程後,線程就會進入可運行狀態<此時線程是可以運行的,但是還沒有真正的運行,只有等待CPU爲其分配資源後線程纔開始運行>
- 臨時阻塞狀態,當線程執行了sleep或者wait方法,就會進入臨時阻塞狀態。
- 死亡狀態(結束狀態):如果一個線程執行完了run()方法,就進入結束狀態。
5. 線程安全問題
單一線程時,它只能在同一時間進行一項操作,所以永遠不必擔心有兩個實體同時使用相同的資源。但是進入多線程環境後,它們就不再是孤立的,可能多個線程試圖在同一時間訪問同一個資源。比如兩個線程同時從一個銀行賬戶取款!
舉一個多線程中常見的例子,三個售票窗口同時賣票,一共50張票,使用以下代碼模擬:
class SaleTicket extends Thread {
// 設置爲靜態變量,因爲是三個線程的貢獻資源數據
static int num = 50;
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "售出了"
+ num + "號票");
num--;
} else {
System.out.println("售罄");
break;
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
SaleTicket a = new SaleTicket("A窗口");
SaleTicket b = new SaleTicket("B窗口");
SaleTicket c = new SaleTicket("C窗口");
a.start();
b.start();
c.start();
}
}
控制檯輸出如下:
A窗口售出了50號票
B窗口售出了50號票
B窗口售出了48號票
...
...
...
可以發現50號票被A、B窗口都賣出了,顯然是錯誤的。這裏就出現了線程安全問題,分析如下:
由控制檯的輸出我們可以做以下假設:
假設A窗口先搶到了CPU的資源,在第5行判斷num>0,打印第6行的內容,此時CPU的資源被B窗口搶走,注意A窗口並沒有執行num–,然後B窗口在第5行判斷num>0,打印第6行的內容,執行num–,此時num等於49,然後CPU的資源被A窗口搶走,此時A窗口應該執行第6行代碼,num–。現在num = 48,B窗口搶奪了CPU的資源,判斷num>0,執行第6行,就會打印出”B窗口售出了48號票”。
在什麼情況下會出現線程安全問題?
**
- 存在多線程
- 存在共享的資源(比如上例中的票)
- 存在多個任務來操作共享資源,當前任務進行到一半時,CPU的資源被別的線程搶奪
**
線程安全問題的解決辦法:
同步機制:調用synchronized方法時對象會被鎖定,不能被其他的線程訪問,除非當前線程完成了被同步的任務,並解除鎖定。
①:同步代碼塊
synchronized("鎖對象"){
//需要被同步的代碼
}
同步代碼塊要注意的事項:
- “鎖對象”可以是任意的一個對象
- 多線程操作的鎖對象必須是唯一的、共享的
- 在同步代碼塊中調用sleep方法,並不會釋放鎖
- 只有存在線程安全問題的時候才使用同步代碼塊,否則會降低線程執行的效率
使用同步代碼塊完成賣票,並解決線程安全問題:
class SaleTicket extends Thread {
// 設置爲靜態變量,因爲是三個線程的貢獻資源數據
static int num = 50;
static Object o = new Object();
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while (true) {
// 同步代碼塊
synchronized (o) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "售出了"
+ num + "號票");
num--;
} else {
System.out.println("售罄");
break;
}
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
SaleTicket a = new SaleTicket("A窗口");
SaleTicket b = new SaleTicket("B窗口");
SaleTicket c = new SaleTicket("C窗口");
a.start();
b.start();
c.start();
}
}
②:同步函數
- 使用synchronized 修飾函數
- 如果是非靜態的同步函數,鎖對象是當前對象;如果是靜態的同步函數,鎖對象是當前函數所屬的類的class對象。
考慮以下代碼能不能解決線程安全的問題?
class BankThread extends Thread {
static int count = 5000;
public BankThread(String name) {
super(name);
}
@Override
public synchronized void run() {
while (true) {
if (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "取走了1000元,還剩餘" + (count - 1000) + "元");
count = count - 1000;
} else {
System.out.println("賬戶餘額不足...");
break;
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
BankThread grandFather = new BankThread("爺爺");
BankThread grandMother = new BankThread("奶奶");
grandMother.start();
grandFather.start();
}
}
控制檯輸出如下:
爺爺取走了1000元,還剩餘4000元
奶奶取走了1000元,還剩餘4000元
奶奶取走了1000元,還剩餘2000元
爺爺取走了1000元,還剩餘1000元
奶奶取走了1000元,還剩餘0元
賬戶餘額不足...
爺爺取走了1000元,還剩餘-1000元
賬戶餘額不足...
因爲synchronized 修飾的函數是非靜態的,所以當線程grandMother執行取錢任務時,鎖對象是grandMother;當grandFather 執行取錢任務時,鎖對象是grandFather 對象。鎖對象不是唯一的就不能解決線程安全問題
推薦使用同步代碼塊解決線程安全問題!!!
6. 死鎖
有時兩個或多個線程需要鎖定幾個共享對象。這時可能引起死鎖(deadlock) ,也就是說,每個線程已經鎖定一個對象,正在等待鎖定另一個對象。考慮一種有兩個線程和兩個對象的情形。線程1已經鎖定了 object1 ,線程2鎖定了 object2 。現在線程1等待鎖定 object2 ,線程2等待鎖定object1 。每個線程都等待另一個線程釋放自己需要的資源,結果導致兩個線程都無法繼續運行。
例如老公拿着銀行卡,但是沒有密碼;老婆記着密碼,但是沒有銀行卡,參考以下代碼:
class DeadLock extends Thread {
public DeadLock(String name) {
super(name);
}
@Override
public void run() {
if ("老公".endsWith(Thread.currentThread().getName())) {
synchronized ("銀行卡") {
System.out.println("老公拿到了銀行卡,等待密碼...");
synchronized ("密碼") {
System.out.println("老公拿到了密碼...");
System.out.println("老公可以去銀行取錢了!!!");
}
}
} else if ("老婆".endsWith(Thread.currentThread().getName())) {
synchronized ("密碼") {
System.out.println("老婆拿到了密碼,等待銀行卡...");
synchronized ("銀行卡") {
System.out.println("老婆拿到了銀行卡...");
System.out.println("老婆可以去銀行取錢了!!!");
}
}
}
}
}
public class Demo3 {
public static void main(String[] args) {
DeadLock thread1 = new DeadLock("老公");
DeadLock thread2 = new DeadLock("老婆");
thread1.start();
thread2.start();
}
}
控制檯輸出如下:
老公拿到了銀行卡,等待密碼...
老婆拿到了密碼,等待銀行卡...
老公進入“銀行卡”的同步代碼塊中,但是此時老婆進入了“密碼”的同步代碼塊中。兩個線程都會陷入無休止的相互等待狀態。儘管這種情況並非經常出現,但一旦碰見,程序的調試就會變得異常艱難。就java語言本身來說,尚未提供防止死鎖的措施,我們需要謹慎設計來避免。
但是上述例子中,只要“老公”線程跑的足夠快,兩者都是可以取到錢的!嘿嘿….
可以考慮將代碼更改爲如下,“老公”和“老婆”就有更大的可能性都能取到錢了!
else if ("老婆".endsWith(Thread.currentThread().getName())) {
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("密碼") {
System.out.println("老婆拿到了密碼,等待銀行卡...");
synchronized ("銀行卡") {
System.out.println("老婆拿到了銀行卡...");
System.out.println("老婆可以去銀行取錢了!!!");
}
}
}
7. 線程間通信
wait():等待,線程執行了wait()方法,就會進入等待狀態,等待狀態的線程必須要被其他線程調用notify()方法才能喚醒。
notify():喚醒,喚醒等待的線程。
Note:
- wait()和notify()屬於Object對象的方法
- wait()和notify()必須要在同步代碼塊或同步函數中才能使用
- wait()和notify()必須要由鎖對象調用
當i爲偶數時生產者生產蘋果,i爲奇數是生產者生產香蕉;生產者生產一個產品消費者消費一個產品。使用以下代碼進行模擬:
//產品類
class Product {
boolean flag = false;// 產品是否已經存在
String name;
double price;
}
// 生產者
class Producer extends Thread {
Product p;
public Producer(Product p) {
this.p = p;
}
// 不斷地生產
@Override
public void run() {
int i = 0;
while (true) {
synchronized (p) {
if (p.flag == false) {
if (i % 2 == 0) {
p.name = "蘋果";
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.price = 5.0;
} else {
p.name = "香蕉";
p.price = 2.5;
}
System.out.println("生產者生產了:" + p.name + "價格是:" + p.price);
p.flag = true;
//生產完畢,喚醒消費者
p.notify();
i++;
} else {
try {
// 生產者已經生產完畢,等待消費者去消費
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
// 消費者
class Customer extends Thread {
Product p;
public Customer(Product p) {
this.p = p;
}
@Override
public void run() {
while (true) {
synchronized (p) {
if (p.flag == true) {
System.out.println("消費者消費了:" + p.name + "價格是:" + p.price);
p.flag = false;
//消費完了,喚醒生產者
p.notify();
}else{
try {
//產品沒有被生產,等待生產者生產
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Demo4 {
public static void main(String[] args) {
Product p = new Product();// 產品
Producer producer = new Producer(p);// 生產者
Customer customer = new Customer(p);// 消費者
producer.start();
customer.start();
}
}
控制檯輸出:
生產者生產了:蘋果價格是:5.0
消費者消費了:蘋果價格是:5.0
生產者生產了:香蕉價格是:2.5
消費者消費了:香蕉價格是:2.5
...
...
說明:
1. 將p作爲構造函數的參數傳入,以實現p的共享;並且讓p作爲鎖對象,調用wait和notify
2. 如果一個線程執行了wait()方法,那麼該線程就會進入一個以”鎖對象”爲標識的線程池中並處於等待狀態
3. 調用wait()方法會釋放鎖對象
4. 如果一個線程執行了notify()方法,那麼就會喚醒上述線程池中的一個處於等待狀態的線程
5. 調用notify()方法不能指定線程來喚醒,一般來說,先等待的線程先被喚醒
8. 停止線程
使用Thread類的stop()方法來停止一個線程,但此方法已經過時,不推薦使用。
如果需要停止一個處於等待狀態的線程,可以通過布爾變量配合notify()或者interrupt()方法。
public class Demo5 extends Thread {
boolean falg = true;
public Demo5(String name) {
super(name);
}
@Override
public synchronized void run() {
int i = 0;
while (falg) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
public static void main(String[] args) {
Demo5 d = new Demo5("線程A");
d.setPriority(10);
d.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
// 主線程i=80是,停止線程A
if (i == 80) {
d.falg = false;
/*同步代碼塊*/
// synchronized (d) {
// d.notify();
// }
/*使用interrupt清除等待狀態,中斷線程*/
d.interrupt();
}
}
}
}
線程A開啓後,flag = true ,線程A會進入等待狀態。然後執行主線程,當i=80,flag賦值爲false,並且喚醒線程A;因爲此時的flag爲false,所以run方法裏的代碼會執行完畢,線程就被停止。
使用interrupt 強制清除處於等待狀態的線程:如果線程在調用Object類的wait()、wait(long)或者wait(long,int)方法,或者Thread類的join()、join(long)、join(long,int)、sleep(long)或者sleep(long,int)方法後,執行interrupt會強制清除這些線程!並返回一個InterruptedException的異常。interrupt還可以指定清除哪個線程。
9. 守護線程和join()
守護線程的作用是在程序運行期間於後臺提供一種“常規”服務,但是它並不屬於程序的一個基本部分。一旦所有的非守護線程完成,程序就會終止,守護線程也會終止。可以調用isDaemon()查看一個線程是否爲守護線程。線程默認不是守護線程,可以使用setDaemon(true)設置一個線程爲守護線程。
以下代碼爲模擬軟件更新包的後臺下載,
public class Demo6 extends Thread {
public Demo6(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("更新包目前下載" + i + "%....");
if (i == 100) {
System.out.println("更新包下載完畢,準備安裝...");
}
}
}
public static void main(String[] args) {
Demo6 d = new Demo6("後臺線程");
//設置爲守護線程
d.setDaemon(true);
System.out.println(d.isDaemon());
d.start();
for(int j = 0;j<100;j++){
System.out.println(Thread.currentThread().getName()+":"+j);
}
}
}
可以發現,當主線程停止時,後臺下載更新的守護線程也會停止。
join():一個線程如果執行了join語句,那麼就有新的線程加入,執行該語句的線程必須要讓步給新加入的線程來完成任務,然後才能繼續執行。
class Mom extends Thread{
@Override
public void run() {
System.out.println("媽媽開始做飯");
System.out.println("媽媽開始洗菜,切菜,炒菜...");
System.out.println("媽媽發現沒有醬油了...讓我去打醬油");
//我去打醬油
Me me = new Me();
me.start();
try {
/**
* mom執行了me.join()語句,me線程就加入到mom線程,並且mom線程會等待me線程執行完畢纔會繼續執行
* */
me.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("媽媽繼續做飯");
System.out.println("全家人開心地吃晚飯...");
}
}
class Me extends Thread{
@Override
public void run() {
System.out.println("我一直往小賣鋪走");
System.out.println("打完醬油...");
System.out.println("回家,並把醬油交給媽媽...");
}
}
public class Demo7 {
public static void main(String[] args) {
Mom mom = new Mom();
mom.start();
}
}
控制檯輸出如下:
媽媽開始做飯
媽媽開始洗菜,切菜,炒菜...
媽媽發現沒有醬油了...讓我去打醬油
我一直往小賣鋪走
打完醬油...
回家,並把醬油交給媽媽...
媽媽繼續做飯
全家人開心地吃晚飯...
最後,希望這篇線程相關的文章可以對java的初學者有所幫助,上文有不當的地方,請大家提出自己寶貴的意見!