java的多線程安全,ReentrantLock與synchronized鎖
前言
多線程總的來說是一個很大的模塊,所以雖然之前就想寫但一直感覺有地方沒有理解透,在經過了一段時間學習後,終於有點感覺了,在此寫下隨筆。
多線程安全問題##:
首先和大家討論一下多線程為什麽會不安全,大家先看下面的程序。
/** - @author lw */ public class Test extends Thread{ public void run() { for(int i=1;i<=10;i++) { System.out.println(i); } } public static void main(String args[]) { Test t1=new Test(); Test t2=new Test(); Test t3=new Test(); Test t4=new Test(); t1.start(); t2.start(); t3.start(); t4.start(); }
上面這段程序大致意思就是新建了四個線程,每個線程的操作都是輸出1-10,按說來應該按線程啟動順序依次輸出,但其實並不是。<--12345678112345678910234567891091012345678910-->這是輸出的結果。線程並沒有順序執行,原因就是線程的搶占。在線程一執行到一半,輸出到8的時候,便被其他線程搶占,其他線程繼續輸出。
這樣的並發會帶來什麽問題呢?大家請看下面這段代碼。
/** - @author lw - */ public class Test extends Thread{ static int temp=1; public void run() { temp++; System.out.println(temp); } public static void main(String args[]) { Test t1=new Test(); Test t2=new Test(); Test t3=new Test(); Test t4=new Test(); t1.start(); t2.start(); t3.start(); t4.start(); } }
大家可以上面的程序大致是定義了一個static的整形變量,然後每一個線程可以對這個變量加1,
首先static變量是全局共享的,每一個線程都能操作這個變量,問題就出在這裏。如果有一次線程1運行,然後讀入了該變量為1,這個時候線程2搶占,然後對該變量進行了加一的操作,此時線程1再繼續運行,但該變量現在已經是2了,線程1讀入的確是之前的1,在加一之後為2,就出問題了。這種問題我們稱為線程之間不同步,因為線程之間的操作互相是不可見的。下面,我們深入討論一下為什麽會這樣。
線程不同步的原因
線程之所以會不同步,本質原因在於每個線程的高速緩存區。每個線程在創建後會有自己的一個緩存區,在線程要訪問主存中的變量的時候會先將主存中的變量加入緩存,然後進行操作,這樣可以避免主存訪問過於頻繁,可以加快線程的執行效率(類似於cache)。但問題在於每個線程的緩存區之間不可見,如果載入的是主存中的同一個變量,分別進行了更改,就會出現線程不同步的問題。
不同步解決策略
好了,上面都是鋪墊,下面才是重點,如何解決線程不同步問題。java給出了鎖的概念。所謂鎖形象一點理解就是一個線程在用一個資源就像一個人進了一扇門,如果不鎖門,其他人也會進來,但如果加了鎖,就意味著這個資源被這個線程獨占,而且必須要退出了才能被其他線程使用。我們常用的就是synchronized鎖,又叫同步鎖。我們先看一下這個鎖的效果。
/**
* @author lw
*
*/
public class Test extends Thread{
String lo="";
public void run()
{
synchronized(lo)
{
for(int i=1;i<=10;i++)
{
System.out.println(i);
}
}
}
public static void main(String args[])
{
Test t1=new Test();
Test t2=new Test();
Test t3=new Test();
Test t4=new Test();
t1.start();
t2.start();
t3.start()
t4.start();
}
}
在經過了上面的同步之後,線程便可以按順序運行,因為在第一個線程開始後,他會獲得變量lo的鎖,然後執行下面的代碼塊,其他線程在得到這個鎖之前會處於一個阻塞狀態,等待第一個線程釋放鎖之後其他線程競爭,然後獲得鎖的線程繼續代碼塊裏的操作,這樣就可以保證線程之間的異步了,接下來我們需要知道的是為什麽加了鎖可以實現同步。
synchronized是如何實現同步的
好吧其實很簡單,比較機智的讀者可能已經猜到了,他其實是使各個線程之間的高速緩存區失效了,然後線程要獲取該變量的時候需要在主存中讀寫,這個時候對該變量的操作對於各個線程之間是可見的,然後操作結束之後再刷新其緩存區,哈哈哈是不是很簡單。。。
synchronized需註意的事項
大家要註意的是synchronized加鎖的目標是對象,並不是代碼塊。這是初學者容易進入的誤區。有人認為只要是synchronized裏面的操作一定不會有問題, 但當這樣想的時候其實你已經涼了。請看下面的代碼。
/**
* @author lw
*
*/
public class Test extends Thread{
String lo=new String();
public void run()
{
synchronized(lo)
{
for(int i=1;i<=10;i++)
{
System.out.println(i);
}
}
}
public static void main(String args[])
{
Test t1=new Test();
Test t2=new Test();
Test t3=new Test();
Test t4=new Test();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
看上去與上面的沒太大差別,但細心的讀者會發現有一行變成了String lo=new String();這個時候的鎖便沒有任何意義,因為這個對象每一個線程都會new一個,也就是說每一個線程都會獲得一個,所以完全不起作用。可能基礎欠佳的同學會問之前的String lo=“”;為什麽可以,因為每一個lo都會指向常量池(常量池這裏不展開講了 ,手要廢了。。不知道的可以百度一下)中的同一個對象,所以每一個線程的還都是指向同一段主存,鎖就會起作用。大家是不是覺得synchonized已經很完美了,no no no還有更完美的,rentrantlock閃亮登場!!!(打字好累啊。。。。#==)
reentrantlock
首先我們討論一下synchonized的缺點。一是不靈活,synchonized在鎖定之後必須要代碼塊結束之後才能釋放鎖,然後被其他線程獲得。那麽如果獲取到鎖的這個線程要執行非常長的時間呢,那其他的線程不是會一直阻塞在這裏,這時如果有哪個線程生氣了不想等了怎麽辦?抱歉不可以,需要一直等待。另一方面,同步鎖的釋放順序也很固定,必須是加鎖的反順序,很不瀟灑等等。。。但我們的reentrantlock就不一樣了,話不多說先看代碼。
/**
* @author lw
*
*/
public class Test extends Thread{
private static ReentrantLock lock =new ReentrantLock();
public void run()
{
try{
lock.lock();
for(int i=1;i<=10;i++)
{
System.out.println(i);
}
}
finally
{
lock.unlock();
}
}
public static void main(String args[])
{
Test t1=new Test();
Test t2=new Test();
Test t3=new Test();
Test t4=new Test();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
上面我們可以看到聲明了ReentrantLock對象後只需調用其中的lock方法便可直接加鎖,而釋放鎖需要unlock方法。這樣一是很靈活,不需要代碼塊結束再釋放,還有就是 ReentrantLock是可中斷的,如果等待的線程不想等了,好說,interrupt掉就好了,另外, ReentrantLock可以設為悲觀鎖和樂觀鎖,而synchonized則默認為悲觀鎖,不可改變,不夠靈活。所以綜上,ReentrantLock更加靈活多變。但大家在使用時一定要記得unlock,最好寫在finally裏面防止忘記,不然就會造成其他線程阻塞。
多線程是一個很大的知識塊,以上是筆者自己學習思考後的總結歸納,還有很多沒有涉及到,另外分享內容如有不當之處望大家多多指正,共同進步~
下期預告
radius緩存
java的多線程安全,ReentrantLock與synchronized鎖