線程是CPU資源調(diào)度的基本單位,如果一個程序中只有一個線程,則最多只能在一個處理器上運行,如果電腦/服務(wù)器是雙處理器系統(tǒng),則單線程的程序只能使用一半的CPU資源,所以,多線程是提高處理器資源利用率的重要方法。比如web系統(tǒng)中的servlet容器,它處理請求時會針對每一個請求創(chuàng)建一個線程調(diào)用servlet的service方法(https://m.runoob.com/servlet/servlet-life-cycle.html),servlet的實例在容器中只會創(chuàng)建一個,所以就涉及到了并發(fā)操作問題(多個線程訪問處理同一個資源),下圖摘自菜鳥教程中給出的一個請求示例:

從上圖中可以看出,如果service()方法對某一個可變的公共變量做了操作,線程a,b,c如果未作合適的同步控制的話,程序必然會出現(xiàn)錯誤,修復(fù)這個問題有三種方式:
a.不在線程之間共享該狀態(tài)變量,也就是service()方法不會調(diào)用公共的變量/資源。
b.將狀態(tài)變量設(shè)置為不可修改的變量,也就是service()方法調(diào)用的公共變量的值是一個恒定的值,各個線程也不能對該變量進(jìn)行修改。
c.在訪問公共的狀態(tài)變量時進(jìn)行同步控制,也就是線程b/c要等線程a對公共變量的操作執(zhí)行完畢之后才能獲取訪問權(quán)。
一個無狀態(tài)的對象一定是線程安全的,如下示例:
package main; import javax.servlet.*; import java.io.IOException; public class StatelessFactorizer implements Servlet { @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { int id=(int)req.getAttribute("id"); id++; req.setAttribute("id",id); } @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
上面的代碼中,id雖然做了++操作,但是它是屬于方法的內(nèi)部變量,也是線程私有的,這個值是從前端帶過來的,所以這是一中“無狀態(tài)的”操作方法。
package main; import javax.servlet.*; import java.io.IOException; public class UnsafeCounting implements Servlet { private long count=0; @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { count++; req.setAttribute("count",count); } @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
再如上面的示例,UnsafeCounting實例的service方法由多個線程調(diào)用時,每個線程都會操作count這個公共資源,但是count++這種操作方法并不是原子性的,所以這種操作不是線程安全的。這里定義一個術(shù)語:
競態(tài)條件(Race Condition):并發(fā)編程時,由于不恰當(dāng)?shù)膱?zhí)行時序而出現(xiàn)不正確的結(jié)果的情況。
對于有競態(tài)條件出現(xiàn)的情況下,需要使用“加鎖機制”來保證操作步驟的原子性,也就是多個線程對操作步驟的順序執(zhí)行,java提供了一種內(nèi)置的鎖機制來實現(xiàn)原子操作:同步代碼塊(synchronized block),同步代碼塊由兩部分組成:
a.加鎖的對象,是一個引用,也就是說對哪個對象進(jìn)行加鎖,確定了加鎖的對象,所有的線程要先拿到這個對象的鎖才能進(jìn)行下一步的操作
b.由該鎖保護(hù)的代碼塊,也就是哪些操作步驟必須是原子操作,就將這些需要作為原子操作的步驟放到鎖所保護(hù)的代碼塊種即可
如下示例:
synchronized(obj){//todo 需要是原子操作的步驟}
**鎖的重入**
重入是一個重要特征,就是一個線程獲取到了某個鎖的執(zhí)行權(quán),它再次獲取該鎖時,需要保證能夠獲取成功,
package main; public class Widget { public synchronized void doSomething(){ System.out.println("Widget doSomething "+this); } public static void main(String[] args) { LoggingWidget loggingWidget=new LoggingWidget(); loggingWidget.doSomething(); } } class LoggingWidget extends Widget{ @Override public synchronized void doSomething() { System.out.println("LoggingWidget doSomething "+this); super.doSomething(); } } ---------輸出-------- LoggingWidget doSomething main.LoggingWidget@74a14482 Widget doSomething main.LoggingWidget@74a14482
上面的代碼中,synchronized方法修飾的是非靜態(tài)方法,它的鎖對象是this,也就是方法調(diào)用所在的對象,如果synchronized放到靜態(tài)方法上,則其加鎖的對象是Class對象;從上面的打印中,super中輸出的this和子類中輸出的this是一樣的,如果synchronized鎖不能重入的話,子類在調(diào)用doSomething的時候已經(jīng)獲取到了“main.LoggingWidget@74a14482”的鎖,就不能調(diào)用super.doSomething()了,顯然是不合理的;鎖的重入則很完美地避免了該問題的產(chǎn)生。
是不是為了簡單起見,直接將synchronized加到方法上得了?當(dāng)然不是,因為還要考慮性能問題,如下實例:
class DownLoadFile extends HttpServlet{ @Override protected synchronized void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //todo 下載一個大文件 File file=new File("c:/xxxx/uuu"); //todo 對某個公共資源進(jìn)行操作 } }
上面的代碼中,下載大文件的操作比較費時,如果對方法進(jìn)行加鎖,則性能很差,這時候可以不對下載大文件進(jìn)行加鎖,如下:
class DownLoadFile extends HttpServlet{ @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //todo 下載一個大文件 File file=new File("c:/xxxx/uuu"); synchronized(this){ //todo 對某個公共資源進(jìn)行操作 } } }
上面的代碼中,性能將會提升很多,因為每個請求可以發(fā)送請求后立馬進(jìn)行文件下載操作,文件下載完畢后再嘗試獲取鎖對公共資源進(jìn)行操作。
以上是Java線程安全性的總結(jié),在保證安全的同時也需要考慮性能問題,因此加鎖時需要做到:
a.識別哪些操作步驟需要是原子性操作,以保證程序的安全性
b.判斷哪些操作比較費時,這些比較費時的操作一定不要持有鎖,因為會嚴(yán)重影響程序性能,記住,只對需要加鎖的步驟進(jìn)行加鎖就可以了
浙公網(wǎng)安備 33010602011771號