會當凌絕頂,一覽衆山小
| @Author:TTODS
Java多線程入門(二)
線程同步
爲什麼要線程同步?
使用多線程可以讓我們的程序更加充分的利用CPU,提高程序的效率,但是同時也帶來了一些問題。多線程在使用線程公共資源的時候往往會遇到問題。
想象一下這樣一個情景:一個商店中有三件商品,但是四個顧客同時來購買商品,他們都能買到商品嗎?
我們用代碼模擬一下:
public class Test{
public static void main(String[] args) {
//創建一個商店
Store store = new Store();
//創建四個共用一個商店的顧客線程
CustomerThread ct1 = new CustomerThread("顧客1",store);
CustomerThread ct2 = new CustomerThread("顧客2",store);
CustomerThread ct3 = new CustomerThread("顧客3",store);
CustomerThread ct4 = new CustomerThread("顧客4",store);
//啓動四個線程
ct1.start();
ct2.start();
ct3.start();
ct4.start();
}
}
//商店類
class Store {
//剩餘商品數量
static int goodsNumble = 3;
public Store(){
}
//出售一件商品
void sell() {
//先判斷商品是否售完
if(goodsNumble<=0)
System.out.println("商店:出售失敗,商品賣完了");
else {
System.out.println("此時還有"+goodsNumble+"件商品");
goodsNumble--;
System.out.println("售出一件商品,商品數量減1");
}
}
}
class CustomerThread extends Thread{
private Store store;
public CustomerThread(String name,Store store) {
// TODO 自動生成的構造函數存根
setName(name);
this.store = store;
}
@Override
public void run() {
buy();
}
//購買商品,調用store的sell方法
public void buy() {
store.sell();
}
}
運行結果
從上面的代碼和運行結果我們可以看到,因爲顧客幾乎是同時到達商店的,所以他們到的時候商品的水量都是3,於是商店給每一位顧客都出售了一件商品。這顯然不是我們想要的結果。然後我們給Store類的sell()方法加上synchronized關鍵字,局部修改如下:
synchronized void sell() {
//先判斷商品是否售完
if(goodsNumble<=0)
System.out.println("商店:出售失敗,商品賣完了");
else {
System.out.println("此時還有"+goodsNumble+"件商品");
goodsNumble--;
System.out.println("售出一件商品,商品數量減1");
}
}
運行結果:
可以看到在我們給sell
方法使用了synchronized
關鍵字後Stroe
會按照一定的順序來爲顧客服務,這就是synchronized
方法的效果.噹噹前線程使用synchronized
方法時,會給當前對象(store)上鎖,上鎖後其他線程在使用相同對象的方法時進入阻塞,需等待此線程中的該方法結束,才能繼續運行。
線程同步的三個方法
爲了介紹線程同步的三個方法,我們先來看一下這樣一個場景:有一個房間,房間裏居住着四個人,四個人共用着房間裏的一部電話。每個人都可以在任何時候使用電話,但是如果他們在同一時間都來使用這部電話又會發生什麼呢?
我們用代碼模擬一下:
public class SynchronizedMethod {
public static void main(String[] args) {
Room r = new Room();
//創建並啓動4個學生線程
StudentThread s1 =new StudentThread("s1",r);
StudentThread s2 =new StudentThread("s2",r);
StudentThread s3 =new StudentThread("s3",r);
StudentThread s4 =new StudentThread("s4",r);
s1.start();
s2.start();
s3.start();
s4.start();
}
}
class Room {
public Phone phone;
public Room() {
// TODO 自動生成的構造函數存根
phone = new Phone();
}
}
class Phone{
static final int FREE = 1,BUSY = 0;
//電話的狀態
private int status=FREE;
public Phone() {
}
//判斷電話是否空閒
public boolean isFree() {
return status==FREE;
}
//打電話,半秒後掛斷,爲了打印電話使用者的名字,我們傳入一個參數
public void call(String user) {
if(!isFree()) {
System.out.println("有人正在使用電話哦");
return;
}
status = BUSY;
System.out.println(user+"打出了電話...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
//掛電話
public void hangUp(String user) {
status = Phone.FREE;
System.out.println(user+"掛斷了電話...");
}
}
class StudentThread extends Thread{
private Room room;
public StudentThread(String name,Room room) {
setName(name);
this.room = room;
}
@Override
public void run() {
this.room.phone.call(getName());
}
}
運行結果
我們可以發現當線程s1使用了對象phone
的call
方法的時候,其他三個線程仍然使用了call方法。這就是線程不同步的效果。接下來我們分別用三種方法實現線程同步。
synchronized
方法
將Phone
類的call
聲明爲sychronized
方法(在call
方法前面加上關鍵字即可),修改部分代碼如下:
public synchronized void call(String user) {
if(!isFree()) {
System.out.println("有人正在使用電話哦");
return;
}
status = BUSY;
System.out.println(user+"打出了電話...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
運行結果
synchronized
塊
在call
函數內添加synchronized
代碼塊,此方法與上一方法相比,優點在於你不需將整個方法都變成同步的,只需把關鍵代碼塊變爲同步即可,修改部分代碼如下:
public void call(String user) {
synchronized (this) {
if(!isFree()) {
System.out.println("有人正在使用電話哦");
return;
}
status = BUSY;
System.out.println(user+"打出了電話...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果
- Lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Phone{
private Lock lock= new ReentrantLock();
static final int FREE = 1,BUSY = 0;
//電話的狀態
private int status=FREE;
public Phone() {
}
//判斷電話是否空閒
public boolean isFree() {
return status==FREE;
}
//打電話,半秒後掛斷,爲了打印電話使用者的名字,我們傳入一個參數
public void call(String user) {
//上鎖
lock.lock();
if(!isFree()) {
System.out.println("有人正在使用電話哦");
return;
}
status = BUSY;
System.out.println(user+"打出了電話...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
e.printStackTrace();
}
//釋放鎖
lock.unlock();
}
//掛電話
public void hangUp(String user) {
status = Phone.FREE;
System.out.println(user+"掛斷了電話...");
}
}
運行結果
線程死鎖
線程死鎖,是指多個線程在使用公共資源時各佔了資源的一部分,導致每個線程都等待其他線程釋放資源才能完成工作,最終導致每個線程都無法完成工作,互相卡死。
想象一下這樣的情景:上學時,你和你的同桌共用一個筆記本和一支鋼筆,只是你的老師講到了一個要點,於是你順勢搶到了鋼筆,而你的同桌拿到了筆記本,誰也不肯讓步,於是誰也無法完成筆記。
我們用代碼模擬一下:
public class DeadLock {
public static void main(String[] args) {
Stationery stationery = new Stationery();
//p1 takeNotes時先獲取並鎖定本子,然後再試圖獲取筆
Person p1 = new Person("p1",stationery) {
@Override
public void takeNotes() {
synchronized (stationery.notebook) {
getNotebook();
synchronized (stationery.pen) {
getPen();
}
}
}
};
//p2 takeNotes時先獲取並鎖定筆,然後再試圖獲取本子
Person p2 = new Person("p2",stationery) {
@Override
public void takeNotes() {
synchronized (stationery.pen) {
getPen();
synchronized (stationery.notebook) {
getNotebook();
}
}
}
};
p1.start();
p2.start();
}
}
//文具類(共用資源)
class Stationery{
//筆
public static Pen pen = new Pen();
//筆記本
public static Notebook notebook= new Notebook();
}
class Pen{
}
class Notebook{
}
abstract class Person extends Thread{
Stationery stationery;
public Person(String name,Stationery s) {
// TODO 自動生成的構造函數存根
setName(name);
stationery = s;
}
//獲取筆
Pen getPen() {
System.out.println(getName()+":我拿打筆了");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
return stationery.pen;
};
//獲取本子
Notebook getNotebook() {
System.out.println(getName()+":我拿打筆記本了");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
return stationery.notebook;
};
//抽象方法
public abstract void takeNotes();
@Override
public void run() {
takeNotes();
};
}
運行結果(程序並沒有結束而是一直卡住無法往下運行)
線程的生命週期
- 新建態 :
Thread t = new Thread()
,線程對象被創建,此時還沒有使用start()方法,啓動線程 - 就緒狀態 :
t.start()
後進入就緒狀態,但具體什麼時候運行取決於CPU的調度 - 運行態:線程佔用CPU運行
- 阻塞態:線程在運行時,使用了
Thread.sleep()
或者進行I/O操作等不在佔用CPU,則由運行態進入阻塞態,待事件結束再進入就緒態 t.stop()
線程結束,進入終止態(消亡狀態)