[Java并發編程:synchronized](http://www.cnblogs.com/dolphin0520/p/3923737.html)
[TOC=1,3]
雖然多線程編程極大地提高了效率,但是也會帶來一定的隱患。比如說兩個線程同時往一個數據庫表中插入不重復的數據,就可能會導致數據庫中插入了相同的數據。今天我們就來一起討論下線程安全問題,以及Java中提供了什么機制來解決線程安全問題。
以下是本文的目錄大綱:
一.什么時候會出現線程安全問題?
二.如何解決線程安全問題?
三.synchronized同步方法或者同步塊
若有不正之處,請多多諒解并歡迎批評指正。
請尊重作者勞動成果,轉載請標明原文鏈接:
http://www.cnblogs.com/dolphin0520/p/3923737.html
## 一.什么時候會出現線程安全問題?
在單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現同時訪問同一個資源的情況,這種資源可以是各種類型的的資源:一個變量、一個對象、一個文件、一個數據庫表等,而當多個線程同時訪問同一個資源的時候,就會存在一個問題:
由于每個線程執行的過程是不可控的,所以很可能導致最終的結果與實際上的愿望相違背或者直接導致程序出錯。
舉個簡單的例子:
現在有兩個線程分別從網絡上讀取數據,然后插入一張數據庫表中,要求不能插入重復的數據。
那么必然在插入數據的過程中存在兩個操作:
1)檢查數據庫中是否存在該條數據;
2)如果存在,則不插入;如果不存在,則插入到數據庫中。
假如兩個線程分別用thread-1和thread-2表示,某一時刻,thread-1和thread-2都讀取到了數據X,那么可能會發生這種情況:
thread-1去檢查數據庫中是否存在數據X,然后thread-2也接著去檢查數據庫中是否存在數據X。
結果兩個線程檢查的結果都是數據庫中不存在數據X,那么兩個線程都分別將數據X插入數據庫表當中。
這個就是線程安全問題,即多個線程同時訪問一個資源時,會導致程序運行結果并不是想看到的結果。
這里面,這個資源被稱為:臨界資源(也有稱為共享資源)。
也就是說,當多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。
不過,當多個線程執行一個方法,方法內部的局部變量并不是臨界資源,因為方法是在棧上執行的,而Java棧是線程私有的,因此不會產生線程安全問題。
## 二.如何解決線程安全問題?
那么一般來說,是如何解決線程安全問題的呢?
基本上所有的并發模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源后釋放鎖,讓其他線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
本文主要講述synchronized的使用方法,Lock的使用方法在下一篇博文中講述。
## 三.synchronized同步方法或者同步塊
在了解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。
舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其他線程便只能等待。
在Java中,每一個對象都擁有一個鎖標記(monitor),也稱為監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
在Java中,可以使用synchronized關鍵字來標記一個方法或者代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程才會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。
下面通過幾個簡單的例子來說明synchronized關鍵字的使用:
1.synchronized方法
下面這段代碼中兩個線程分別調用insertData對象插入數據:
~~~
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(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
~~~
此時程序的輸出結果為:

說明兩個線程在同時執行insert方法。
而如果在insert方法前面加上關鍵字synchronized的話,運行結果為:
~~~
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public synchronized void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
~~~

從上輸出結果說明,Thread-1插入數據是等Thread-0插入完數據之后才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
這就是synchronized方法。
不過有幾點需要注意:
1)當一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法。這個原因很簡單,因為一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
2)當一個線程正在訪問一個對象的synchronized方法,那么其他線程能訪問該對象的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該對象的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那么其他線程是可以訪問這個方法的,
3)如果一個線程A需要訪問對象object1的synchronized方法fun1,另外一個線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型),也不會產生線程安全問題,因為他們訪問的是不同的對象,所以不存在互斥問題。
2.synchronized代碼塊
synchronized代碼塊類似于以下這種形式:
`synchronized(synObject) {`
`}`
當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。
synObject可以是this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。
比如上面的insert方法可以改成以下兩種形式:
`class`?`InsertData {`
`private`?`ArrayList<Integer> arrayList =?``new`?`ArrayList<Integer>();`
`public`?`void`?`insert(Thread thread){`
`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(Thread thread){
synchronized(object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
`}`
`}`
`}`
`}`
從上面可以看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。因為也許一個方法中只有一部分代碼只需要同步,如果此時對整個方法用synchronized進行同步,會影響程序執行效率。而使用synchronized代碼塊就可以避免這個問題,synchronized代碼塊可以實現只對需要同步的地方進行同步。
另外,每個類也會有一個鎖,它可以用來控制對static數據成員的并發訪問。
并且如果一個線程執行一個對象的非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完畢"``);`
`}`
`}`
執行結果;

第一個線程里面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。
下面我們看一下synchronized關鍵字到底做了什么事情,我們來反編譯它的字節碼看一下,下面這段代碼反編譯后的字節碼為:
`public`?`class`?`InsertData {`
`private`?`Object object =?``new`?`Object();`
`public`?`void`?`insert(Thread thread){`
`synchronized`?`(object) {`
`}`
`}`
`public`?`synchronized`?`void`?`insert1(Thread thread){`
`}`
`public`?`void`?`insert2(Thread thread){`
`}`
`}`
|

從反編譯獲得的字節碼可以看出,synchronized代碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執行時會讓對象的鎖計數加1,而monitorexit指令執行時會讓對象的鎖計數減1,其實這個與操作系統里面的PV操作很像,操作系統里面的PV操作就是用來控制多個線程對臨界資源的訪問。對于synchronized方法,執行中的線程識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設置,然后它自動獲取對象的鎖,調用方法,最后釋放鎖。如果有異常發生,線程自動釋放鎖。
有一點要注意:對于synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程占用的鎖,因此不會由于異常導致出現死鎖現象。
參考資料:
《Java編程思想》
[http://ifeve.com/synchronized-blocks/](http://ifeve.com/synchronized-blocks/)
[http://ifeve.com/java-synchronized/](http://ifeve.com/java-synchronized/)
[http://blog.csdn.net/ns_code/article/details/17199201](http://blog.csdn.net/ns_code/article/details/17199201)
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)