并發編程總結之synchronized細節問題
摘要:本節主要介紹了并發編程下怎么避免數據臟讀和什么是synchronized的可重入鎖,synchronized的可重入鎖的幾種使用場景下,是線程安全的。
臟讀
什么是臟讀
對于對象的同步和異步方法,我們在設計程序,一定要考慮問題的整體性,不然會出現數據不一致的錯誤,最經典的錯誤就是臟讀(DirtyRead)。
示例Code
/**
* 業務整體需要使用完整的synchronized,保持業務的原子性。
*
* @authorxujin
*
*/
publicclassDirtyRead{
privateString username ="xujin";
privateString password ="123";
publicsynchronizedvoidsetValue(String username, String password){
this.username = username;
try{
Thread.sleep(2000);
} catch(InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最終結果:username = "+ username +" , password = "+ password);
}
//①這里getValue沒有加synchronized修飾
publicvoidgetValue(){
System.out.println("getValue方法得到:username = "+this.username +" , password = "+this.password);
}
publicstaticvoidmain(String[] args)throwsException{
finalDirtyRead dr =newDirtyRead();
Thread t1 = newThread(newRunnable() {
@Override
publicvoidrun(){
dr.setValue("張三","456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
上面的Code中,getValue沒有加synchronized修飾,打印結果如下,出現臟讀
getValue方法得到:username = 張三 , password = 123
setValue最終結果:username = 張三 , password = 456
只需在getValue加synchronized修飾,如下:
publicsynchronizedvoidgetValue(){
System.out.println("getValue方法得到:username = "+this.username +" , password = "+this.password);
}
運行結果如下,沒有造成數據臟讀
setValue最終結果:username = 張三 , password = 456
getValue方法得到:username = 張三 , password = 456
小結
在我們對對象中的一個方法加鎖的時候,需要考慮業務的或程序的整體性,也就是為程序中的set和get方法同時加鎖synchronized同步關鍵字,保證業務的(service層)的原子性,不然會出現數據錯誤,臟讀。
synchronized的重入
什么是synchronized的重入鎖
- synchronized,它擁有強制原子性的內置鎖機制,是一個重入鎖,所以在使用synchronized時,當一個線程請求得到一個對象鎖后再次請求此對象鎖,可以再次得到該對象鎖,就是說在一個synchronized方法/塊的內部調用本類的其他synchronized方法/塊時,是永遠可以拿到鎖。
- 當線程請求一個由其它線程持有的對象鎖時,該線程會阻塞,而當線程請求由自己持有的對象鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞.
簡單的說:關鍵字synchronized具有 鎖重入 的功能,也就是在使用 synchronized時 , 當一個線程 得到一個 對象鎖 的 鎖后 , 再次請求此對象時 可以 再次 得到該 對象對應的鎖 。
嵌套調用關系synchronized的重入
嵌套調用關系synchronized的重入也是線程安全的,下面是method1,method2,method3都被synchronized修飾,調用關系method1–>method2–>method3,也是線程安全的。
/**
* synchronized的重入
*
* @authorxujin
*
*/
publicclassSyncReenTrant{
publicsynchronizedvoidmethod1(){
System.out.println("method1..");
method2();
}
publicsynchronizedvoidmethod2(){
System.out.println("method2..");
method3();
}
publicsynchronizedvoidmethod3(){
System.out.println("method3..");
}
publicstaticvoidmain(String[] args){
finalSyncReenTrant sd =newSyncReenTrant();
Thread t1 = newThread(newRunnable() {
@Override
publicvoidrun(){
sd.method1();
}
});
t1.start();
}
運行結果如下:
method1..
method2..
method3..
繼承關系的synchronized的重入
簡單 Code1:
public class Son extends Father {
public synchronized void doSomething() {
System.out.println("child.doSomething()");
// 調用自己類中其他的synchronized方法
doAnotherThing();
}
private synchronized void doAnotherThing() {
// 調用父類的synchronized方法
super.doSomething();
System.out.println("child.doAnotherThing()");
}
public static void main(String[] args) {
Son child = new Son();
child.doSomething();
}
}
class Father {
public synchronized void doSomething() {
System.out.println("father.doSomething()");
}
}
運行結果:
child.doSomething()
father.doSomething()
child.doAnotherThing()
- 這里的對象鎖只有一個,就是child對象的鎖,當執行child.doSomething時,該線程獲得child對象的鎖,在doSomething方法內執行doAnotherThing時再次請求child對象的鎖,因為synchronized是重入鎖,所以可以得到該鎖,繼續在doAnotherThing里執行父類的doSomething方法時第三次請求child對象的鎖,同理可得到,如果不是重入鎖的話,那這后面這兩次請求鎖將會被一直阻塞,從而導致死鎖。
- 所以在Java內部,同一線程在調用自己類中其他synchronized方法/塊或調用父類的synchronized方法/塊都不會阻礙該線程的執行,就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。因為java線程是基于“每線程(per-thread)”,而不是基于“每調用(per-invocation)”的(java中線程獲得對象鎖的操作是以每線程為粒度的,per-invocation互斥體獲得對象鎖的操作是以每調用作為粒度的)
我們再來看看重入鎖是怎么實現可重入性的,其實現方法是為每個鎖關聯一個線程持有者和計數器,當計數器為0時表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調用相應的方法;當某一線程請求成功后,JVM會記下鎖的持有線程,并且將計數器置為1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,如果計數器為0,則釋放該鎖。
publicclassSyncExtends{
// 父類
staticclassFather{
publicinti =10;
publicsynchronizedvoidoperationSup(){
try{
i--;
System.out.println("Father print i = "+ i);
Thread.sleep(100);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
// 子類繼承父類
staticclassSonextendsFather{
publicsynchronizedvoidoperationSub(){
try{
while(i >0) {
i--;
System.out.println("Son print i = "+ i);
Thread.sleep(100);
this.operationSup();
}
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
publicstaticvoidmain(String[] args){
Thread t1 = newThread(newRunnable() {
@Override
publicvoidrun(){
Son sub = newSon();
sub.operationSub();
}
});
t1.start();
}
}
運行結果如下:
Son print i = 9
Father print i = 8
Son print i = 7
Father print i = 6
Son print i = 5
Father print i = 4
Son print i = 3
Father print i = 2
Son print i = 1
Father print i = 0
參考文章:
http://blog.csdn.net/aigoogle/article/details/29893667
來自:http://blog.xujin.org/2016/11/26/bf/bf-synchronized/