鎖是整個Java並發包的實現基礎,通過學習本系列文章,將對你理解Java並發包的本質有很大的幫助。
前邊幾篇中,我已經把實現鎖用到的技術,進行了一一講述。這其中有原子性、內存模型、LockSupport還有CAS,掌握了這些技術,即使沒有本篇,你也完全有能力自己寫一把鎖出來。但為了本系列的完整性,我在這里還是把最后這一篇補上。
先說一下鎖的運行流程:多個線程搶占同一把鎖,只有一個線程能搶占成功,搶占成功的線程繼續執行下邊的邏輯,搶占失敗的線程進入阻塞等待。搶占成功的線程執行完畢后,釋放鎖,並從等待的線程中挑一個喚醒,讓它繼續競爭鎖。
轉變成程序實現:我們首先定一個state變量,state=0表示未被加鎖,state=1表示被加鎖。多個線程在搶占鎖時,競爭將state變量從0修改為1,修改成功的線程則加鎖成功。state從0修改為1的過程,這里使用cas操作,以保證只有一個線程加鎖成功,同時state需要用volatile修飾,已解決線程可見的問題。加鎖成功的線程執行完業務邏輯后,將state從1修改回0,同時從等待的線程中選擇一個線程喚醒。所以加鎖失敗的線程,在加鎖失敗時需要將自己放到一個集合中,以等待被喚醒。這個集合需要支持多線程並發安全,在這里我通過一個鏈表來實現,通過CAS操作來實現並發安全。
把思路說清楚了,咱們看下代碼實現。
首先咱們實現一個ThreadList,這是一個鏈表結合,用來存放等待的處於等待喚醒的線程:
public class ThreadList{ private volatile Node head = null; private static long headOffset; private static Unsafe unsafe; static { try { Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]); constructor.setAccessible(true); unsafe = constructor.newInstance(new Object[0]); headOffset = unsafe.objectFieldOffset(ThreadList.class.getDeclaredField("head")); }catch (Exception e){ } } /** * * @param thread * @return 是否只有當前一個線程在等待 */ public boolean insert(Thread thread){ Node node = new Node(thread); for(;;){ Node first = getHead(); node.setNext(first); if(unsafe.compareAndSwapObject(this, headOffset,first,node)){ return first==null?true:false; } } } public Thread pop(){ Node first = null; for(;;){ first = getHead(); Node next = null; if(first!=null){ next = first.getNext(); } if(unsafe.compareAndSwapObject(this, headOffset,first,next)){ break; } } return first==null?null:first.getThread(); } private Node getHead(){ return this.head; } private static class Node{ volatile Node next; volatile Thread thread; public Node(Thread thread){ this.thread = thread; } public void setNext(Node next){ this.next = next; } public Node getNext(){ return next; } public Thread getThread(){ return this.thread; } } }
加鎖失敗的線程,調用insert方法將自己放入這個集合中,insert方法里將線程封裝到Node中,然后使用cas操作將node添加到列表的頭部。同樣為了線程可見的問題,Node里的thread和next都用volatile修飾。
加鎖成功的線程,調用pop方法獲得一個線程,進行喚醒,這里邊同樣使用了cas操作來保證線程安全。
接下來在看看鎖的實現:
public class MyLock { private volatile int state = 0; private ThreadList threadList = new ThreadList(); private static long stateOffset; private static Unsafe unsafe; static { try { Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]); constructor.setAccessible(true); unsafe = constructor.newInstance(new Object[0]); stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state")); }catch (Exception e){ } } public void lock(){ if(compareAndSetState(0,1)){ }else{ addNodeAndWait(); } } public void unLock(){ compareAndSetState(1,0); Thread thread = threadList.pop(); if(thread != null){ LockSupport.unpark(thread); } } private void addNodeAndWait(){ //如果當前只有一個等待線程時,重新獲取一下鎖,防止永遠不被喚醒。 boolean isOnlyOne = threadList.insert(Thread.currentThread()); if(isOnlyOne && compareAndSetState(0,1)){ return; } LockSupport.park(this);//線程被掛起 if(compareAndSetState(0,1)){//線程被喚醒后繼續競爭鎖 return; }else{ addNodeAndWait(); } } private boolean compareAndSetState(int expect,int update){ return unsafe.compareAndSwapInt(this,stateOffset,expect,update); } }
線程調用lock方法進行加鎖,cas將state從0修改1,修改成功則加鎖成功,lock方法返回,否則調用addNodeAndWait方法將線程加入ThreadList隊列,並使用LockSupport將線程掛起。(ThreadList的insert方法,返回一個boolean類型的值,用來處理一個特殊情況的,稍后再說。)
獲得鎖的線程執行完業務邏輯后,調用unLock方法釋放鎖,即通過cas操作將state修改回0,同時從ThreadList拿出一個等待線程,調用LockSupport的unpark方法,來將它喚醒。
將我們在《自己動手寫把"鎖"---鎖的作用》的例子修改為如下,來測試下咱們的鎖的效果:
public class TestMyLock { private static List<Integer> list = new ArrayList<>(); private static MyLock myLock = new MyLock(); public static void main(String[] args){ Thread t1 = new Thread(new Runnable() { @Override public void run() { for(int i=0;i<10000;i++){ add(i); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { print(); } }); t1.start(); t2.start(); } private static void add(int i){ myLock.lock(); list.add(i); myLock.unLock(); } private static void print(){ myLock.lock(); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } myLock.unLock(); } }
ok,正常運行了,不在報錯。
到這里咱們的一個簡單地鎖已經實現了。接下來我再把上邊的,一個沒講的細節說一下。即如下這段代碼:
boolean isOnlyOne = threadList.insert(Thread.currentThread()); if(isOnlyOne && compareAndSetState(0,1)){ return; }
ThreadList的insert方法,在插入成功后,會判斷當前鏈表中是否只有自己一個線程在等待,如果是則返回true。從而進入后邊的if語句。這個邏輯的用意就是:如果只有自己一個線程在等待時,則試着通過cas操作重新獲取鎖,如果獲取失敗才進入阻塞等待。它是用來解決以下邊界情況:
在只有線程A和線程B兩個線程的時候,如果沒有以上判斷邏輯,線程B將有可能會永遠處於阻塞不被喚醒。
以下是本系列其他的文章:
-------------------------------------------------
有興趣的朋友,可以加入我的知識圈,一起研究討論。
我正在「JAVA互聯網技術」和朋友們討論有趣的話題,你一起來吧?