Java並發編程之線程安全、線程通信
Java多線程開發中最重要的一點就是線程安全的實現了。所謂Java線程安全,可以簡單理解為當多個線程訪問同一個共享資源時產生的數據不一致問題。為此,Java提供了一系列方法來解決線程安全問題。
synchronized
synchronized用於同步多線程對共享資源的訪問,在實現中分為同步代碼塊和同步方法兩種。
同步代碼塊
1 public class DrawThread extends Thread { 2 3 private Account account; 4 private double drawAmount; 5 public DrawThread(String name, Account account, doubledrawAmount) { 6 super(name); 7 this.account = account; 8 this.drawAmount = drawAmount; 9 } 10 @Override 11 public void run() { 12 //使用account作為同步代碼塊的鎖對象 13 synchronized(account) { 14 if (account.getBalance() >= drawAmount) { 15 System.out.println(getName() + "取款成功, 取出:" + drawAmount);16 try { 17 TimeUnit.MILLISECONDS.sleep(1); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 account.setBalance(account.getBalance() - drawAmount); 22 System.out.println("余額為: " + account.getBalance());23 } else { 24 System.out.println(getName() + "取款失敗!余額不足!"); 25 } 26 } 27 } 28 }
同步方法
使用同步方法,即使用synchronized關鍵字修飾類的實例方法(非static方法),可以實現線程安全類,即該類在多線程訪問中,可以保證可變成員的數據一致性。
1 public class SyncAccount { 2 private String accountNo; 3 private double balance; 4 //省略構造器、getter setter方法 5 //在一個簡單的賬戶取款例子中, 通過添加synchronized的draw方法, 把Account類變為一個線程安全類 6 public synchronized void draw(double drawAmount) { 7 if (balance >= drawAmount) { 8 System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount); 9 try { 10 TimeUnit.MILLISECONDS.sleep(1); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 balance -= drawAmount; 15 System.out.println("余額為: " + balance); 16 } else { 17 System.out.println(Thread.currentThread().getName() + "取款失敗!余額不足!"); 18 } 19 } 20 //省略HashCode和equals方法 21 }
同步鎖(Lock、ReentrantLock)
Java5新增了兩個用於線程同步的接口Lock和ReadWriteLock,並且分別提供了兩個實現類ReentrantLock(可重入鎖)和ReentrantReadWriteLock(可重入讀寫鎖)。
Java8新增了更為強大的可重入讀寫鎖StampedLock類。
比較常用的是ReentrantLock類,可以顯示地加鎖、釋放鎖。下面使用ReentrantLock重構上面的SyncAccount類。
1 public class RLAccount { 2 //定義鎖對象 3 private final ReentrantLock lock = new ReentrantLock(); 4 private String accountNo; 5 private double balance; 6 //省略構造方法和getter setter 7 public void draw(double drawAmount) { 8 //加鎖 9 lock.lock(); 10 try { 11 if (balance >= drawAmount) { 12 System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount); 13 try { 14 TimeUnit.MILLISECONDS.sleep(1); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 balance -= drawAmount; 19 System.out.println("余額為: " + balance); 20 } else { 21 System.out.println(Thread.currentThread().getName() + "取款失敗!余額不足!"); 22 } 23 } finally { 24 //通過finally塊保證釋放鎖 25 lock.unlock(); 26 } 27 } 28 }
死鎖
當兩個線程相互等待地方釋放鎖的時候,就會產生死鎖。
線程通信方式之wait、notify、notifyAll
Object類提供了三個用於線程通信的方法,分別是wait、notify和notifyAll。這三個方法必須由同步鎖對象來調用,具體來說:
1. 同步方法:因為同步方法默認使用所在類的實例作為鎖,即this,可以在方法中直接調用。
2. 同步代碼塊:必須由鎖來調用。
wait():導致當前線程等待,直到其它線程調用鎖的notify方法或notifyAll方法來喚醒該線程。調用wait的線程會釋放鎖。
notify():喚醒任意一個在等待的線程
notifyAll():喚醒所有在等待的線程
1 /* 2 * 通過一個生產者-消費者隊列來說明線程通信的基本使用方法 3 * 註意: 假如這裏的判斷條件為if語句,喚醒方法為notify, 那麽如果分別有多個線程操作入隊\出隊, 會導致線程不安全. 4 */ 5 public class EventQueue { 6 7 private final int max; 8 9 static class Event{ 10 11 } 12 //定義一個不可改的鏈表集合, 作為隊列載體 13 private final LinkedList<Event> eventQueue = new LinkedList<>(); 14 15 private final static int DEFAULT_MAX_EVENT = 10; 16 17 public EventQueue(int max) { 18 this.max = max; 19 } 20 21 public EventQueue() { 22 this(DEFAULT_MAX_EVENT); 23 } 24 25 private void console(String message) { 26 System.out.printf("%s:%s\n",Thread.currentThread().getName(), message); 27 } 28 //定義入隊方法 29 public void offer(Event event) { 30 //使用鏈表對象作為鎖 31 synchronized(eventQueue) { 32 //在循環中判斷如果隊列已滿, 則調用鎖的wait方法, 使線程阻塞 33 while(eventQueue.size() >= max) { 34 try { 35 console(" the queue is full"); 36 eventQueue.wait(); 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 } 41 console(" the new event is submitted"); 42 eventQueue.addLast(event); 43 this.eventQueue.notifyAll(); 44 } 45 } 46 //定義出隊方法 47 public Event take() { 48 //使用鏈表對象作為鎖 49 synchronized(eventQueue) { 50 //在循環中判斷如果隊列已空, 則調用鎖的wait方法, 使線程阻塞 51 while(eventQueue.isEmpty()) { 52 try { 53 console(" the queue is empty."); 54 eventQueue.wait(); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 } 59 Event event = eventQueue.removeFirst(); 60 this.eventQueue.notifyAll(); 61 console(" the event " + event + " is handled/taked."); 62 return event; 63 } 64 } 65 }
線程通信方式之Condition
如果使用的是Lock接口實現類來同步線程,就需要使用Condition類的三個方法實現通信,分別是await、signal和signalAll,使用上與Object類的通信方法基本一致。
1 /* 2 * 使用Lock接口和Condition來實現生產者-消費者隊列的通信 3 */ 4 public class ConditionEventQueue { 5 //顯示定義Lock對象 6 private final Lock lock = new ReentrantLock(); 7 //通過newCondition方法獲取指定Lock對象的Condition實例 8 private final Condition cond = lock.newCondition(); 9 private final int max; 10 static class Event{ } 11 //定義一個不可改的鏈表集合, 作為隊列載體 12 private final LinkedList<Event> eventQueue = new LinkedList<>(); 13 private final static int DEFAULT_MAX_EVENT = 10; 14 public ConditionEventQueue(int max) { 15 this.max = max; 16 } 17 18 public ConditionEventQueue() { 19 this(DEFAULT_MAX_EVENT); 20 } 21 22 private void console(String message) { 23 System.out.printf("%s:%s\n",Thread.currentThread().getName(), message); 24 } 25 //定義入隊方法 26 public void offer(Event event) { 27 lock.lock(); 28 try { 29 //在循環中判斷如果隊列已滿, 則調用cond的wait方法, 使線程阻塞 30 while (eventQueue.size() >= max) { 31 try { 32 console(" the queue is full"); 33 cond.await(); 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 } 38 console(" the new event is submitted"); 39 eventQueue.addLast(event); 40 cond.signalAll();; 41 } finally { 42 lock.unlock(); 43 } 44 45 } 46 //定義出隊方法 47 public Event take() { 48 lock.lock(); 49 try { 50 //在循環中判斷如果隊列已空, 則調用cond的wait方法, 使線程阻塞 51 while (eventQueue.isEmpty()) { 52 try { 53 console(" the queue is empty."); 54 cond.wait(); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 } 59 Event event = eventQueue.removeFirst(); 60 cond.signalAll(); 61 console(" the event " + event + " is handled/taked."); 62 return event; 63 } finally { 64 lock.unlock(); 65 } 66 } 67 }
事實上,Java5開始就提供了BlockingQueue接口,來實現如上所述的生產者-消費者線程同步工具。具體介紹將另文說明。
Java並發編程之線程安全、線程通信