1 線程安全問題
思考這樣一個問題:
單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現多個線程同時訪問同一個臨界資源(或共享資源:一個變量、一個對象、一個文件、一個數據庫表)情況,多個線程併發執行過程不可控,很可能導致最終的結果與實際上的願望相違背或者直接導致程序出錯。
例如:
當線程A讀取到一個數據D的時候,然後開始使用,但是有可能在使用前,線程B改變了數據D,導致線程A讀取使用的數據和此時實際的數據值不一致(數據庫中叫做讀取了髒數據)。還有很多其他的情況,這裏不一一列舉了。
那麼如何解決線程安全問題的呢?
大部分併發模式解決線程安全問題都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常採取的做法:在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問該臨界資源。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
2 synchronized方式
先明白一個概念:互斥鎖---達到多個線程對同一個資源互斥訪問的目的。
Java中的對象都有一個鎖標記(monitor,也叫監視器),當多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
而synchronized作爲一個Java中的關鍵字,可以通過給一個對象標記一個方法或代碼塊,來達到加互斥鎖的目的;當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的互斥鎖,其他線程暫時無法訪問該對象,處於等待狀態。只有當前有用互斥鎖的線程釋放該對象的鎖後,其他線程才能執行這個方法或者代碼塊。
2.1 synchrozied修飾方法
下面是一個兩個線程同時插入數據的例子:
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Threadthread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
結果:兩個線程同時調用了InsertData對象的insert方法插入數據,並且過程是不可控的,兩者插入順序也不可控制,可能出現交叉的插入順序;
如果給insert方法加上synchronized關鍵字修飾:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public synchronized void insert(Threadthread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
結果:只有線程0調用該方法執行完畢之後,釋放了互斥鎖,線程1才能拿到鎖調用該對象方法。
總結:
1)當一個線程正在訪問一個對象的synchronized方法,那麼其他線程不能訪問該對象的其他synchronized方法(使用到臨界資源);能訪問該對象的非synchronized方法(不會使用到臨界資源)。
2)如果線程A需要訪問對象object1的synchronized方法fun1,另外線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型,也不會產生線程安全問題,因爲他們訪問的是不同的對象,不存在互斥問題。
3)另外,如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因爲訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是對象鎖,所以不存在互斥現象。
上述第三點的例子如下:
public class Test {
public static void main(String[]args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
public synchronized void insert(){
System.out.println("執行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行insert完畢");
}
public synchronized static void insert1() {
System.out.println("執行insert1");
System.out.println("執行insert1完畢");
}
}
從上述執行結果可以知道:訪問非static synchronized方法佔用的對象鎖,不會阻塞訪問static synchronized方法佔用的類鎖,可以理解爲類鎖的優先級更高。
2.2 synchronized修飾代碼塊
Synchronized修飾代碼塊的形式: synchronized(synObject){ 代碼 }
當線程A執行這段代碼塊時,會獲取對象Object的互斥鎖,則其他線程無法同時訪問;Object可以是this也可以是當前對象的一個屬性,表示當前對象的鎖和該屬性的鎖。
例如:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Threadthread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();
public void insert(Threadthread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
}
較之synchronized方法,synchronized代碼塊更加靈活;因爲一個方法中可能只需要同步一部分代碼,如果此時對整個方法用synchronized進行同步,會影響程序執行效率。而使用synchronized代碼塊就可以有針對性的同步需要同步的地方,提高執行效率。
總結兩種方式:
1) 每個類也會有一個鎖,它可以用來控制對static數據成員的併發訪問。
2) synchronized方法和synchronized代碼塊出現異常時,JVM會自動釋放當前線程佔用的鎖,不會出現死鎖現象。
3 Lock方式
既然可以通過synchronized來實現同步訪問了,爲什麼又提供Lock方式了呢?
是因爲Synchronized方式有自己的缺陷,synchronized是Java語言內置的關鍵字。
使用synchronized修飾的方法或代碼塊,被一個線程佔用的時候,只能通過兩種方式釋放鎖:執行完畢釋放鎖和發生異常JVM讓線程釋放鎖。
影響效率?
但是,如果擁有鎖的線程可能因爲某種原因一直等待資源而而不釋放鎖,導致其他線程一直阻塞無法得到鎖,這樣很影響執行效率。
再比如說:有的方法代碼塊可以多線程同時訪問,但是不會影響到彼此,又因爲synchronized修飾只能單線程訪問,這樣也很影響效率。
但是, Lock提供了比synchronized更多的功能:
1)Lock不是Java語言內置的, Lock是一個類,通過這個類可以實現同步訪問,synchronized是Java語言內置的關鍵字。
2)synchronized方式在方法或代碼塊執行前後線程自動加減鎖;而Lock方式則必須要用戶去手動釋放鎖,否則就有可能導致出現死鎖現象(發生異常也不會自動釋放鎖)。
3)Lock方式的lockInterruptibly()方法獲取某個鎖時,等待狀態的線程可以響應中斷;但是synchronized方式的等待線程無法被中斷,只有一直等待下去。
3.1 Lock
Lock類位於java.util.concurrent.locks包下,是一個接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
ConditionnewCondition();
}
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的;unLock()方法是用來釋放鎖的。
1)lock()方法 是平常使用得最多的獲取鎖的形式,如果鎖已被其他線程獲取,則進行等待。
因爲Lock形式不管是否發生異常,必須手動去釋放鎖。因此,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中以保證鎖一定被被釋放,防止死鎖發生。
Lock同步形式:
Lock lock = ...;
lock.lock();
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
2)tryLock()方法 是有返回值的---true表示鎖獲取成功,false表示鎖獲取失敗(即鎖已被其他線程獲取);即使拿不到鎖也不會一直等待,會立即返回。
獲取鎖的形式:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務
}catch(Exceptionex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
3)lockInterruptibly()方法 比較特殊,表示處於等待獲取鎖的線程可以中斷其等待狀態。線程B使用lock.lockInterruptibly()獲取鎖時處於等待狀態,此時可以使用threadB.interrupt()方法中斷線程B的等待過程。形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
但是,獲取了鎖的線程不會被interrupt()方法中斷的---單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
3.2 ReentrantLock
ReentrantLock,意思是“可重入鎖”,是唯一實現了Lock接口的類,並且提供了更多的方法。
使用例子:
1) lock()的正確使用方法
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread){
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
//TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
注意:lock不能作爲局部變量,要作爲類變量,否則每個線程調用了該方法都會保存局部變量--副本,都會獲取不同的鎖,不會互斥,也不會發生衝突,就失去了鎖的意義。
2)lockInterruptibly()響應中斷的使用方法:
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[]args) {
Test test = new Test();
MyThreadthread1 = new MyThread(test);
MyThreadthread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly();
//注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然後將InterruptedException拋出
try {
System.out.println(thread.getName()+"得到了鎖");
long startTime =System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis()- startTime >= Integer.MAX_VALUE)
break;
//插入數據
}
}
finally {
System.out.println(Thread.currentThread().getName()+"執行finally");
lock.unlock();
System.out.println(thread.getName()+"釋放了鎖");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test= test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedExceptione) {
System.out.println(Thread.currentThread().getName()+"被中斷");
}
}
}
2. 3 ReadWriteLock
ReadWriteLock是一個接口,只定義了兩個方法:
public interface ReadWriteLock {
Lock readLock(); //獲取讀鎖
Lock writeLock();//獲取寫鎖
}
將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。
2.4 ReentrantReadWriteLock
ReentrantReadWriteLock類實現了ReadWriteLock接口,提供了很多豐富的方法,主要的方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
ReentrantReadWriteLock具體用法:
如果多個線程同時讀取操作,使用synchronized關鍵字只有一個線程處於讀取狀態,其他都處於等待獲取鎖的狀態,但是使用ReentrantReadWriteLock類的讀寫鎖就不同了,多個線程可以同時進行讀操作,可以大大的提升效率。
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis()- start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
} finally {
rwl.readLock().unlock();
}
}
}
注意:
1)如果有一個線程已經佔用了讀鎖,則此時其他線程如果要申請寫鎖,則要等待讀鎖被釋放。
2)如果有一個線程已經佔用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則要等待寫鎖被釋放。
這兩種性質都防止了讀取的數據被纂改,讀取到不一致的數據。
4 synchronized和Lock的選擇
Lock和synchronized有以下幾點不同:
1)synchronized是Java中的關鍵字,由內置的語言實現;Lock是一個接口。
2)synchronized會在方法或代碼塊前後自動加解鎖,發生異常時JVM會自動釋放線程佔有的鎖,不會導致死鎖現象發生;而Lock需要手動加解鎖,發生異常時,需要在finally塊中通過lock.unLock()方法釋放鎖,否則可能造成死鎖。
3)使用synchronized時,等待的線程會一直等待下去,不能響應中斷;Lock可以讓等待鎖的線程響應中斷。
4)synchronized方式無法得知是否獲取到鎖,Lock方式則可以辦到。
5)相比於synchronized方式,Lock方式可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
5 鎖
1.可重入鎖
具備可重入性的鎖稱作可重入鎖;synchronized和ReentrantLock都是可重入鎖。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程已經擁有了當前對象的鎖,如果去重新申請鎖,就會出現得不到鎖的現象,所以不必重新去申請鎖可以直接執行synchronized方法method2。
所以,可以把可重入性理解爲:以線程爲分配主體的鎖分配機制。
2.可中斷鎖
顧名思義,可以相應中斷的鎖就是可中斷鎖;synchronized不是可中斷鎖,而Lock是可中斷鎖。
前面lock.lockInterruptibly()方法的用法就體現了Lock的可中斷性,具體用法前面已經敘述過了。
3.公平鎖
公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證按順序獲取鎖,這樣就可能導致某個或者一些線程永遠獲取不到鎖。
synchronized就是非公平鎖,Lock方式的兩個實現類ReentrantLock和ReentrantReadWriteLock默認情況下也是非公平鎖,都無法保證等待的線程獲取鎖的順序。
但是Lock方式可以設置爲公平鎖:
ReentrantLocklock = new ReentrantLock(true);
因爲ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。
默認情況下,如果使用無參構造器,則是非公平鎖。
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置爲公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。
4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖:讀鎖和寫鎖,才使得多個線程之間的讀操作不會發生衝突。
讀寫鎖ReadWriteLock是一個接口,ReentrantReadWriteLock類則實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。