本文不打算介紹過多多線程的基本知識,旨在總結一下使用多線程中需要注意的東西。
大家都知道要寫多線程代碼可以實現Runnable接口或者繼承Thread類。關于二者的區別,大家都知道java是單繼承機制的,繼承了Thread可能會讓你無法再繼承其他父類。可能沒有考慮內存相關的問題,導致多線程失效。
直接用代碼來討論,以一個賣票系統為例,總票數15張,模擬3個窗口同時賣票。
實現Runnable
/**
* 不加入同步
* @author
*
*/
public class Ticket1 implements Runnable {
// 總票數
private int total = 15;
public void run() {
while(true) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
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);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
這段代碼的問題很明顯,沒有加入同步控制,輸出大致如下:
t3 sales ticket_15
t2 sales ticket_14
t1 sales ticket_13
t2 sales ticket_12
t3 sales ticket_11
t1 sales ticket_10
t3 sales ticket_9
t1 sales ticket_7
t2 sales ticket_8
t3 sales ticket_6
t1 sales ticket_5
t2 sales ticket_4
t2 sales ticket_3
t3 sales ticket_2
t1 sales ticket_1
t2 sales ticket_0
t3 sales ticket_-1
加入同步控制保證買票正常:
package com.cmsz.upay.thread;
/**
* 加入同步
* @author
*
*/
public class Ticket2 implements Runnable {
// 總票數
private int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket2 t = new Ticket2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
這段代碼可以保證賣票正常:
t1 sales ticket_15
t1 sales ticket_14
t1 sales ticket_13
t1 sales ticket_12
t1 sales ticket_11
t1 sales ticket_10
t1 sales ticket_9
t1 sales ticket_8
t1 sales ticket_7
t1 sales ticket_6
t1 sales ticket_5
t3 sales ticket_4
t3 sales ticket_3
t3 sales ticket_2
t3 sales ticket_1
繼承Thread
先上一段有問題的代碼,應該是比較多初學者不會考慮到的:
/**
* 繼承thread
* @author
*
*/
public class Ticket3 extends Thread {
// 總票數
private int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket3 t1 = new Ticket3();
Ticket3 t2 = new Ticket3();
Ticket3 t3 = new Ticket3();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
運行結果大致如下:
t2 sales ticket_15
t1 sales ticket_15
t3 sales ticket_15
t3 sales ticket_14
t2 sales ticket_14
t1 sales ticket_14
t2 sales ticket_13
t3 sales ticket_13
t1 sales ticket_13
t3 sales ticket_12
t2 sales ticket_12
t1 sales ticket_12
t2 sales ticket_11
t3 sales ticket_11
t1 sales ticket_11
t2 sales ticket_10
t1 sales ticket_10
t3 sales ticket_10
t1 sales ticket_9
t2 sales ticket_9
t3 sales ticket_9
t1 sales ticket_8
t2 sales ticket_8
t3 sales ticket_8
t2 sales ticket_7
t1 sales ticket_7
t3 sales ticket_7
t2 sales ticket_6
t1 sales ticket_6
t3 sales ticket_6
t2 sales ticket_5
t1 sales ticket_5
t3 sales ticket_5
t2 sales ticket_4
t3 sales ticket_4
t1 sales ticket_4
t2 sales ticket_3
t3 sales ticket_3
t1 sales ticket_3
t2 sales ticket_2
t1 sales ticket_2
t3 sales ticket_2
t2 sales ticket_1
t3 sales ticket_1
t1 sales ticket_1
很明顯每個線程都賣了15張票,跟我們預期的不符合,我們在學習面向對象的時候知道了兩個概念:類變量和實例變量,不展開細說二者的區別,實例變量就是每new一個實例出來之后,變量都會有一個自己的內存區域,不受他人影響;而類變量就是所有通過該類實例化的對象共享一個變量。
Ticket3
的問題之處就是將票總數total
聲明為實例變量了,這樣我們每新建一個實例(線程)total
都會有一個自己的內存區域,所以每次賣的都是自己那15張票。
OK,發現問題解決問題,將票數聲明為類變量問題應該就能解決了,試一下:
package com.cmsz.upay.thread;
/**
* 繼承thread,共享變量
* @author
*
*/
public class Ticket4 extends Thread {
// 總票數
private static int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket4 t1 = new Ticket4();
Ticket4 t2 = new Ticket4();
Ticket4 t3 = new Ticket4();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
運行結果如下:
t2 sales ticket_15
t1 sales ticket_14
t3 sales ticket_13
t1 sales ticket_12
t2 sales ticket_11
t3 sales ticket_10
t2 sales ticket_9
t1 sales ticket_8
t3 sales ticket_7
t2 sales ticket_5
t1 sales ticket_6
t3 sales ticket_4
t2 sales ticket_3
t1 sales ticket_2
t3 sales ticket_1
t1 sales ticket_0
t2 sales ticket_-1
坑爹的問題出現了,雖然是大家共同賣15張票,但是賣出了0和-1號的票,也就是我們買了17張票,現實是這樣的話估計搶座要打起來了……
分析一下原因,共享變量沒問題,那就是同步出現問題了,同步有什么問題呢?
問題出現在synchronized(this)
,獲取鎖的對象是this,很明顯3個Thread的this對象是不同的,說白了就是這個加鎖根本沒有鎖住共享變量。
知道了問題的原因,我們只要保證synchronized(syncObject)
中的syncObject
唯一即可,聲明一個類變量即可。
/**
* 繼承thread,共享變量,鎖相同對象
* @author
*
*/
public class Ticket5 extends Thread {
private static Object syncObject = new Object();
// 總票數
private static int total = 15;
public void run() {
while(true) {
synchronized (syncObject) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket5 t1 = new Ticket5();
Ticket5 t2 = new Ticket5();
Ticket5 t3 = new Ticket5();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
運行即可保證正常。
本文總結主要針對編寫多線程代碼過程中共享變量的問題和鎖機制的一些細節,初學者容易犯錯或者欠考慮,通過幾個Demo的總結,可以把一些零散的知識點匯總在一起,保證看問題的全面性。