如果一個資源被多個線程同時訪問,可能會遭到破壞,這篇文章介紹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(); } } } }