如果一個資源被多個線程同時訪問,可能會遭到破壞,這篇文章介紹java線程同步來解決這類問題
某電影院目前正在上映賀歲大片,共有100張票,而它有3個售票窗口售票,請設計一個程序模擬該電影院售票。
方法一:繼承Thread類
public class SellTicket extends Thread {
// 定義100張票
// private int tickets = 100;
// 為了讓多個線程對象共享這100張票,我們其實應該用靜態修飾
private static int tickets = 100;
@Override
public void run() {
// 定義100張票
// 是為了模擬一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" + (tickets--) + "張票");
}
}
}
}
/*
* 繼承Thread類來實現。
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 創建三個線程對象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();
// 給線程對象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
// 啟動線程
st1.start();
st2.start();
st3.start();
}
}
方法二:實現Runnable接口
public class SellTicket implements Runnable {
// 定義100張票
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "張票");
}
}
}
}
/*
* 實現Runnable接口的方式實現
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 創建資源對象
SellTicket st = new SellTicket();
// 創建三個線程對象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 啟動線程
t1.start();
t2.start();
t3.start();
}
}
電影院售票程序,從表面上看不出什麼問題,在真實生活中,售票時網絡是不能實時傳輸的,總是存在延遲的情況,所以,在出售一張票以後,需要一點時間的延遲
改實現接口方式的賣票程序,每次賣票延遲100毫秒,代碼如下:
public class SellTicket implements Runnable {
// 定義100張票
private int tickets = 100;
@Override
public void run() {
while (true) {
// t1,t2,t3三個線程
// 這一次的tickets = 1;
if (tickets > 0) {
// 為了模擬更真實的場景,我們稍作休息
try {
Thread.sleep(100); //t1進來了並休息,t2進來了並休息,t3進來了並休息,
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "張票");
//窗口1正在出售第1張票,tickets=0
//窗口2正在出售第0張票,tickets=-1
//窗口3正在出售第-1張票,tickets=-2
}
}
}
}
/*
* 實現Runnable接口的方式實現
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 創建資源對象
SellTicket st = new SellTicket();
// 創建三個線程對象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 啟動線程
t1.start();
t2.start();
t3.start();
}
}
出現問題:
相同的票出現多次
CPU的一次操作必須是原子性的
還出現了負數的票
隨機性和延遲導致的
首先想為什麼出現問題?(也是我們判斷是否有問題的標准)
如何解決多線程安全問題呢?
基本思想:讓程序沒有安全問題的環境。
把多個語句操作共享數據的代碼給鎖起來,讓任意時刻只能有一個線程執行即可。
解決線程安全問題實現1--同步代碼塊
格式:synchronized(對象){需要同步的代碼;}
同步可以解決安全問題的根本原因就在那個對象上。該對象如同鎖的功能。
修改上面的代碼如下:
public class SellTicket implements Runnable {
// 定義100張票
private int tickets = 100;
//創建鎖對象
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票");
}
}
}
}
}
/*
* 同步代碼塊:
* synchronized(對象){
* 需要同步的代碼;
* }
*
* A:對象是什麼呢?
* 我們可以隨便創建一個對象試試。
* B:需要同步的代碼是哪些呢?
* 把多條語句操作共享數據的代碼的部分給包起來
*
* 注意:
* 同步可以解決安全問題的根本原因就在那個對象上。該對象如同鎖的功能。
* 多個線程必須是同一把鎖。
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 創建資源對象
SellTicket st = new SellTicket();
// 創建三個線程對象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 啟動線程
t1.start();
t2.start();
t3.start();
}
}
注意:同步代碼塊可以用任意對象做鎖
解決線程安全問題實現2--同步方法
就是把同步關鍵字加到方法上
1、同步方法的鎖對象:this
public class SellTicket implements Runnable {
private static int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
}
}
private synchronized void sellTicket() {
if(tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
}
2、靜態方法的鎖對象:類的字節碼文件對象。
public class SellTicket implements Runnable {
private static int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (SellTicket.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
}
}
private static synchronized void sellTicket() {
if(tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
}
同步的前提:
同步的好處:同步的出現解決了多線程的安全問題。
同步的弊端:當線程相當多時,因為每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程序的運行效率。
解決線程安全問題實現3--Lock鎖的使用
雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖對象Lock
ReentrantLock (Java Platform SE 6)
一個可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行為和語義,但功能更強大。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
// 定義票
private int tickets = 100;
// 定義鎖對象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加鎖
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票");
}
} finally {
// 釋放鎖
lock.unlock();
}
}
}
}