多線程(線程安全、線程同步、等待喚醒機制、單例設計模式)


 多線程

今日內容介紹

u 線程安全

u 線程同步

u 死鎖

u Lock鎖

u 等待喚醒機制

第1章 多線程

1.1 線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的

l 我們通過一個案例,演示線程的安全問題:

電影院要賣票,我們模擬電影院的票過程假設要播放的電影是功夫熊貓3”,本次電影的座位共100(電影只能賣100張票)

我們來模擬電影院的售票窗口,實現多個窗口同時賣功夫熊貓3”這場電影票(多個窗口一起賣這100張票)

需要窗口采用線程對象來模擬需要票,Runnable接口子類來模擬

l 測試類

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();

}

}

l 模擬票

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 票出現了重復的票

錯誤的票 0、-1

 

其實,線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

 

1.2 線程同步(線程安全處理Synchronized

java中提供了線程同步機制,它能夠解決上述的線程安全問題。

線程同步的方式有兩種:

方式1:同步代碼塊

方式2:同步方法

1.2.1 同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上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--);

}

}

}

}

}

當使用了同步代碼塊后,上述的線程的安全問題,解決了。

1.2.2 同步方法

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--);

}

}

}

 

靜態同步方法: 在方法聲明上加上static synchronized

public static synchronized void method(){

可能會產生線程安全問題的代碼

}

靜態同步方法中的鎖對象是 類名.class

 

1.3 死鎖

同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程序出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A){

synchronized(B){

         

}

}

 

我們進行下死鎖情況的代碼演示:

l 定義鎖對象類

public class MyLock {

public static final Object lockA = new Object();

public static final Object lockB = new Object();

}

 

l 線程任務類

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++;

}

}

}

 

l 測試類

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();

}

}

1.4 Lock接口

查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。

l 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();

//}

}

}

}

 

1.5 等待喚醒機制

在開始講解等待喚醒機制之前,有必要搞清一個概念——線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

l wait() :等待,將正在執行的線程釋放其執行資格 和 執行權,並存儲到線程池中。

l notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。

l notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。

其實,所謂喚醒的意思就是讓 線程池中的線程具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程。

仔細查看JavaAPI之后,發現這些方法 並不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,為什么這些操作線程的方法定義在Object類中?

因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。

 

 

接下里,我們先從一個簡單的示例入手:

 

 

如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:

l 1.當input發現Resource中沒有數據時,開始輸入,輸入完成后,叫output來輸出。如果發現有數據,就wait();

l 2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然后,叫醒input來輸入數據。

 

下面代碼模擬等待喚醒機制的實現:

l 模擬資源類

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();

}

}

 

l 輸入線程任務類

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;

}

}

}

 

l 輸出線程任務類

public class Output implements Runnable {

private Resource r;

 

public Output(Resource r) {

this.r = r;

}

 

@Override

public void run() {

while (true) {

r.out();

}

}

}

 

l 測試類

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();

}

}

 

 

第2章 總結

2.1 知識點總結

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  : 執行該線程對象要執行的任務

 

l sleep()和wait()方法的區別

sleep: 不釋放鎖對象, 釋放CPU使用權

在休眠的時間內,不能喚醒

wait(): 釋放鎖對象, 釋放CPU使用權

在等待的時間內,能喚醒

 

為什么wait(),notify(),notifyAll()等方法都定義在Object類中

鎖對象可以是任意類型的對象

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM