摘要:本節主要介紹了併發編程下怎麼避免數據髒讀和什麼是synchronized的可重入鎖,synchronized的可重入鎖的幾種使用場景下,是線程安全的。以及一些細節的synchronized使用問題和synchronized常見代碼塊示例Code可以直接Copy運行。
-
髒讀
什麼是髒讀:
對於對象的同步和異步方法,我們在設計程序,一定要考慮問題的整體性,不然會出現數據不一致的錯誤,最經典的錯誤就是髒讀(DirtyRead)。
示例Code:
業務整體需要使用完整的synchronized,保持業務的原子性。
/**
* 業務整體需要使用完整的synchronized,保持業務的原子性。
*
* @author xujin
*
*/
public class DirtyRead {
private String username = "xujin";
private String password = "123";
<!--more-->
public synchronized void setValue(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修飾
public void getValue() {
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
public static void main(String[] args) throws Exception {
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("張三", "456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
上面的Code中,getValue沒有加synchronized修飾,打印結果如下,出現髒讀
getValue方法得到:username = 張三 , password = 123
setValue最終結果:username = 張三 , password = 456
只需在getValue加synchronized修飾,如下:
public synchronized void getValue() {
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的重入
*
* @author xujin
*
*/
public class SyncReenTrant {
public synchronized void method1() {
System.out.println("method1..");
method2();
}
public synchronized void method2() {
System.out.println("method2..");
method3();
}
public synchronized void method3() {
System.out.println("method3..");
}
public static void main(String[] args) {
final SyncReenTrant sd = new SyncReenTrant();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
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,則釋放該鎖。
public class SyncExtends {
// 父類
static class Father {
public int i = 10;
public synchronized void operationSup() {
try {
i--;
System.out.println("Father print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 子類繼承父類
static class Son extends Father {
public synchronized void operationSub() {
try {
while (i > 0) {
i--;
System.out.println("Son print i = " + i);
Thread.sleep(100);
this.operationSup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Son sub = new Son();
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
synchronized常見代碼塊:
1:synchronized可以使用任意的Object進行加鎖, 使用synchronized代碼塊加鎖,比較靈活,如下代碼所示:
public class ObjectLock {
public void method1() {
// 對this當前ObjectLock實例對象加鎖
synchronized (this) {
try {
System.out.println("do method1..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2() {
// 對ObjectLock類加鎖
synchronized (ObjectLock.class) {
try {
System.out.println("do method2..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 任何對象鎖
private Object anyObjectlock = new Object();
public void method3() {
synchronized (anyObjectlock) {
try {
System.out.println("do method3..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ObjectLock objLock = new ObjectLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method2();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method3();
}
});
t1.start();
t2.start();
t3.start();
}
}
-
2:使用synchronized聲明的方法在某些情況下,是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待很長的時間纔可以執行,這樣情況下可以使用synchronize的去優化代碼執行時間,也就是我們通常所說的減小鎖的粒度。
public class Optimize {
public void doLongTimeTask() {
try {
System.out.println("當前線程開始:" + Thread.currentThread().getName() + ", 正在執行一個較長時間的業務操作,其內容不需要同步");
Thread.sleep(2000);
// 使用synchronized代碼塊減小鎖的粒度,提高性能
synchronized (this) {
System.out.println("當前線程:" + Thread.currentThread().getName() + ", 執行同步代碼塊,對其同步變量進行操作");
Thread.sleep(1000);
}
System.out.println("當前線程結束:" + Thread.currentThread().getName() + ", 執行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final Optimize otz = new Optimize();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
}, "t2");
t1.start();
t2.start();
}
}
執行結果:
當前線程開始:t1, 正在執行一個較長時間的業務操作,其內容不需要同步
當前線程開始:t2, 正在執行一個較長時間的業務操作,其內容不需要同步
當前線程:t2, 執行同步代碼塊,對其同步變量進行操作
當前線程結束:t2, 執行完畢
當前線程:t1, 執行同步代碼塊,對其同步變量進行操作
當前線程結束:t1, 執行完畢
3:注意就是不要使用String的常量加鎖,會出現死循環問題。
synchronized代碼塊對字符串的鎖,注意String常量池的緩存功能,示例代碼如下:
public class StringLock {
public void method() {
synchronized ("字符串常量") {
try {
while(true){
System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始");
Thread.sleep(1000);
System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t2");
t1.start();
t2.start();
}
}
提示:運行結果是:t1線程一直死循環。t2線程不執行。修改爲如下代碼,t1和t2線程交替執行
public void method() {
//把synchronized ("字符串常量") 修改爲synchronized (new String("字符串常量"))
synchronized (new String("字符串常量")) {
try {
while (true) {
System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始");
Thread.sleep(1000);
System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.鎖對象的改變問題:
當使用一個對象進行加鎖的時候,要注意對象本身發生變化的時候,那麼持有的鎖就不同。如果對象本身不發生改變,那麼依然是同步的,即使是對象的屬性發生了變化。
示例代碼1:對象本身發生變化的時候,那麼對象持有的鎖就發生變化
public class ChangeLock {
private String lock = "lock";
private void method() {
synchronized (lock) {
try {
System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始");
// 這裏把鎖的內容改變了,因此t1,t2線程基本同時進來,而不是t1休眠2秒後,t2進來
lock = "change lock";
Thread.sleep(2000);
System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
示例代碼2:同一對象屬性的修改不會影響鎖的情況
public class ModifyLock {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void changeAttributte(String name, int age) {
try {
System.out.println("當前線程 : " + Thread.currentThread().getName() + " 開始");
this.setName(name);
this.setAge(age);
System.out.println("當前線程 : " + Thread.currentThread().getName() + " 修改對象內容爲: " + this.getName() + ", "
+ this.getAge());
Thread.sleep(2000);
System.out.println("當前線程 : " + Thread.currentThread().getName() + " 結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final ModifyLock modifyLock = new ModifyLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("許進", 25);
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("李四X", 21);
}
}, "t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
運行結果:
當前線程 : t1 開始
當前線程 : t1 修改對象內容爲: 許進, 25
當前線程 : t1 結束
當前線程 : t2 開始
當前線程 : t2 修改對象內容爲: 李四X, 21
當前線程 : t2 結束