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