Lock介面
Synchronized關鍵字回顧:
多執行緒程式設計步驟(上):
- 建立資源類,在資源類建立屬性和操作方法
- 建立多個執行緒,呼叫資源類的操作方法
建立執行緒的四種方式:
- 繼承Thread
- 實現Runnable介面
- 使用Callable介面
- 使用執行緒池
使用synchronized同步實現售票問題:
只有當資源是空閒的時候,執行緒才能訪問。
/**
* 建立資源
*/
class ticket{
private int number = 30;
public synchronized void sell(){
if(number >0){
System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
}
}
}
/**
* 建立多個執行緒,呼叫資源類的操作方法
*/
public class sellTicket {
public static void main(String[] args) {
/**
* 建立資源
*/
ticket ticket = new ticket();
/**
* 建立三個執行緒(售票員)
*/
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}
}, "aaa").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}
}, "bbb").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sell();
}
}
}, "ccc").start();
}
}
學習《Java併發程式設計的藝術》一節是synchronized的實現原理與應用:
Java中的每一個物件都可以作為鎖:
- 對於同步方法,鎖的是當前的物件。
- 對於靜態方法,鎖的是當前類的class物件。
- 對於靜態程式碼塊,Synchonized括號裡配置的物件。
當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或丟擲異常時必須釋放鎖。
那麼鎖到底存在哪裡呢?鎖裡面會儲存什麼資訊呢?
這個問題是該書中的一個問題,對個人來說有點抽象,主要點是:JVM基於進入和退出Monitor(監視器)物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。
程式碼塊同步是使用monitorenter 和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有 詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結 束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有 一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。執行緒執行到monitorenter 指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。
其中monitorenter主要是獲取監視器鎖,monitorexit主要是釋放監視器鎖。
synchonized中的一個概念是Java物件頭:
synchronized用的鎖是存放在Java物件頭裡面的。
非陣列型別 | 陣列型別 | |
虛擬機器 | 3個字寬 | 2個字寬 |
需要注意的是:在32位虛擬機器中:1字寬=4位元組,即:32bit
Java物件頭裡的Mark Word裡面預設儲存物件HashCode、分代年齡和鎖標記位。
在執行期間,Mark Word裡面儲存的資料會隨著鎖標誌位的變化而變化。
32位JVM的Mark Word的預設儲存結構與在64位JVM不相同
Lock介面並建立例項:
Java併發程式設計的藝術
在JavaSE5之前使用的是synchronized關鍵字實現鎖功能,5v之後併發包中新增Lock介面以及相關實現類用來實現鎖功能,提供與synchronized類似的同步關係,但是比其更具有靈活性和擴充套件性(擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖)。
檢視Lock的文件得知:
使用 lock
塊來呼叫 try
,在之前/之後的構造中,最典型的程式碼如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
根據上述的程式碼,實現買票程式碼:
import java.util.concurrent.locks.ReentrantLock;
class ticket2{
private int number = 30;
//建立一個重入鎖
private ReentrantLock lock = new ReentrantLock();
public void sale(){
//在內容之前加鎖
lock.lock();
if(number >0){
System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
}
//在內容之後解鎖
lock.unlock();
}
}
public class lockCase {
public static void main(String[] args) {
ticket2 tk = new ticket2();
//建立執行緒
new Thread(()->{
for (int i = 0; i < 40; i++) {
tk.sale();
}
},"aaa").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
tk.sale();
}
},"bbb").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
tk.sale();
}
},"bbb").start();
}
}
上述程式碼中,在ticket2類中存在一個問題就是,當lock中間內容如果出現報錯,那麼後面的程式碼無法執行,也就是鎖無法釋放。所以我們需要將unlock()放到finally中。
目的是保證在獲取到鎖之後,最終能夠被釋放。
public void sale(){
//在內容之前加鎖
lock.lock();
try{
if(number >0){
System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
}
//在內容之後解鎖
}finally{
lock.unlock();
}
}