多線程
線程安全
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,演示線程的安全問題:
電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “金瓶梅”,本次電影的座位共100個(本場電影只能賣100張票)。
我們來模擬電影院的售票窗口,實現多個窗口同時賣 “功夫熊貓3”這場電影票(多個窗口一起賣這100張票)
需要窗口,採用線程對象來模擬;需要票,Runnable接口子類來模擬
測試類
public class ThreadDemo {
public static void main(String[] args) {
//創建票對象
Ticket ticket = new Ticket();
//創建3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
模擬票
public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模擬賣票
while(true){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
運行結果發現:上面程序出現了問題
l 票出現了重複的票
l 錯誤的票 0、-1
其實,線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
線程同步(線程安全處理Synchronized)
java中提供了線程同步機制,它能夠解決上述的線程安全問題。
線程同步的方式有兩種:
l 方式1:同步代碼塊
l 方式2:同步方法
同步代碼塊
同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {
可能會產生線程安全問題的代碼
}
同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。
使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖對象
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步代碼塊
synchronized (lock){
if (ticket > 0) {
//模擬電影選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
當使用了同步代碼塊後,上述的線程的安全問題,解決了。
同步方法
l 同步方法:在方法聲明上加上synchronized
public synchronized void method(){
可能會產生線程安全問題的代碼
}
同步方法中的鎖對象是 this
使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖對象
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步方法
method();
}
}
//同步方法,鎖對象this
public synchronized void method(){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
l 靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){
可能會產生線程安全問題的代碼
}
靜態同步方法中的鎖對象是 類名.class
死鎖
同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程序出現無限等待,這種現象我們稱爲死鎖。這種情況能避免就避免掉。
synchronzied(A鎖){
synchronized(B鎖){
}
}
我們進行下死鎖情況的代碼演示:
定義鎖對象類
public class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
線程任務類
public class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定線程要執行的任務代碼
@Override
public void run() {
while(true){
if (x%2 ==0) {
//情況一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if大口吃肉");
}
}
} else {
//情況二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else大口吃肉");
}
}
}
x++;
}
}
}
測試類
public class ThreadDemo {
public static void main(String[] args) {
//創建線程任務類對象
ThreadTask task = new ThreadTask();
//創建兩個線程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//啓動線程
t1.start();
t2.start();
}
}
Lock接口
查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
Lock接口中的常用方法
Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//創建Lock鎖對象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模擬賣票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
ck.unlock();
//}
}
}
}
等待喚醒機制
在開始講解等待喚醒機制之前,有必要搞清一個概念——線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
等待喚醒機制所涉及到的方法:
wait() :等待,將正在執行的線程釋放其執行資格 和 執行權,並存儲到線程池中。
notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。
notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。
其實,所謂喚醒的意思就是讓 線程池中的線程具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣纔可以明確出這些方法操作的到底是哪個鎖上的線程。
仔細查看JavaAPI之後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,爲什麼這些操作線程的方法定義在Object類中?
因爲這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。
接下里,我們先從一個簡單的示例入手:
如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:
1.當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。如果發現有數據,就wait();
2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然後,叫醒input來輸入數據。
下面代碼,模擬等待喚醒機制的實現:
模擬資源類
public class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if (flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 設置成員變量
this.name = name;
this.sex = sex;
// 設置之後,Resource中有值,將標記該爲 true ,
flag = true;
// 喚醒output
this.notify();
}
public synchronized void out() {
if (!flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 輸出線程將數據輸出
System.out.println("姓名: " + name + ",性別: " + sex);
// 改變標記,以便輸入線程輸入數據
flag = false;
// 喚醒input,進行數據輸入
this.notify();
}
}
輸入線程任務類
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int count = 0;
while (true) {
if (count == 0) {
r.set("小加", "男生");
} else {
r.set("小蒼", "女生");
}
// 在兩個數據之間進行切換
count = (count + 1) % 2;
}
}
}
輸出線程任務類
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
測試類
public class ResourceDemo {
public static void main(String[] args) {
// 資源對象
Resource r = new Resource();
// 任務對象
Input in = new Input(r);
Output out = new Output(r);
// 線程對象
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 開啓線程
t1.start();
t2.start();
}
}
知識點總結
l 同步鎖
多個線程想保證線程安全,必須要使用同一個鎖對象
l 同步代碼塊
synchronized (鎖對象){
可能產生線程安全問題的代碼
}
同步代碼塊的鎖對象可以是任意的對象
l 同步方法
public synchronized void method()
可能產生線程安全問題的代碼
}
同步方法中的鎖對象是 this
l 靜態同步方法
public synchronized void method()
可能產生線程安全問題的代碼
}
靜態同步方法中的鎖對象是 類名.class
多線程有幾種實現方案,分別是哪幾種?
a, 繼承Thread類
b, 實現Runnable接口
c, 通過線程池,實現Callable接口
同步有幾種方式,分別是什麼?
a,同步代碼塊
b,同步方法
靜態同步方法
啓動一個線程是run()還是start()?它們的區別?
啓動一個線程是start()
區別:
start: 啓動線程,並調用線程中的run()方法
run : 執行該線程對象要執行的任務
sleep()和wait()方法的區別
sleep: 不釋放鎖對象, 釋放CPU使用權
在休眠的時間內,不能喚醒
wait(): 釋放鎖對象, 釋放CPU使用權
在等待的時間內,能喚醒
l 爲什麼wait(),notify(),notifyAll()等方法都定義在Object類中
鎖對象可以是任意類型的對象