文章目錄
- 髒讀
- 擴充資料庫一緻性的例子(oracle)
- 鎖重入的問題
- 異常釋放鎖的機制
- sycn減小鎖的粒度
- 對象鎖
- String鎖的問題
- 鎖改變的問題
- 死鎖
髒讀
對于對象的同步和異步方法,我們在設計自己程式員的時候,一定要考慮問題的整體性,不然就會出現資料不一緻的錯誤,很經典的錯誤就是髒讀。也就是資料的一緻性問題。存在多個線程工作(主線程,子線程),當一個線程(子線程)在設定值的時候,比如這個過程需要2秒,在1s時其他線程進來擷取值了。此時就産生了髒讀,讀取的資料是不正确的。
錯誤現象:因為一個用了同步,一個沒用同步,這實際上是一套關聯的業務(整體性),别人還沒改完,就不能讀。現象是先出來get,一秒之後出set。
解決的方法也比較簡單,兩個方法同時加上同步關鍵字。這樣就不會出現髒讀了,正常操作就是get/set方法需要加上關鍵字可以防止髒讀這也是一個思路,原理就是實作讀寫互斥。
package com.bjsxt.base.sync004;
/**
* 業務整體需要使用完整的synchronized,保持業務的原子性。
* @author alienware
*
*/
public class DirtyRead {
private String username = "bjsxt";
private String password = "123";
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);
}
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("z3", "456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
擴充資料庫一緻性的例子(oracle)
資料庫的中很多資料1000W。9點發了一條查詢語句,需要到9.10分才能查到那條資料(結果值為100)。但是在9.05分執行了一條DML語句(update),把值改為了200.請問select語句傳回的結果是?
傳回的結果一定是100。oracle資料庫有一緻性的概念(一緻性讀),9點的查詢語句,永遠看到的是9點那一瞬間的結果。(避免了髒讀)oracle中有undo的概念,好比日志,當用戶端執行dml(資料修改語句)語句時,會把老值放入undo,然後修改,如果失敗,則從undo中復原。9點的那天查詢語句,發現值對應的undo有變化(現在值和undo不一樣)了,就會去undo裡面找老值。如果能找到,就傳回,找不到就抛異常。甯肯抛異常,也不會把新值200給用戶端看、這個抛出的異常叫做snapshot too old的經典異常(快照太舊)。這個問題展現了oracle一緻性讀的特性。
鎖重入的問題
關鍵字sync有鎖重入的功能,也就是在使用sync時,當一個線程得到了一個對象的鎖之後,再次請求此對象時可以再次得到該對象的鎖。
/**
* synchronized的重入
*
*/
public class SyncDubbo1 {
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 SyncDubbo1 sd = new SyncDubbo1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
除了上面這個案例還可以使用父子嵌套重入鎖,父類有個sync方法,子類有個sync方法,子類中的sync方法調用了父類的sync方法,這種也叫作重入鎖,利用sync關鍵字,也是線程安全的。
如果有一個沒用用關鍵字修飾,就是線程不安全的。
package com.bjsxt.base.sync005;
/**
* synchronized的重入
* @author alienware
*
*/
public class SyncDubbo2 {
static class Main {
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Main print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub(){
try {
while(i > 0) {
i--;
System.out.println("Sub 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() {
Sub sub = new Sub();
sub.operationSub();
}
});
t1.start();
}
}
異常釋放鎖的機制
被sync修飾的方法遇到異常,會釋放鎖,此時其他線程會進入。這種情況如果多個任務不相關還好。
但是一旦有業務關系,就需要妥善處理(記錄日志是必須的)。不處理的話可以加continue讓他繼續。如果出錯了,後面一定不能執行,就抛出一個運作時異常,讓他停止。
package com.bjsxt.base.sync005;
/**
* synchronized异常
* @author alienware
*
*/
public class SyncException {
private int i = 0;
public synchronized void operation(){
while(true){
try {
i++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , i = " + i);
if(i == 20){
//Integer.parseInt("a");
throw new RuntimeException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SyncException se = new SyncException();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
},"t1");
t1.start();
}
}
sycn減小鎖的粒度
使用sync聲明的方法在某些情況下是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待比較長的時間才能執行,這樣的情況可以使用sync代碼塊去優化代碼執行的時間,也就是通常所說的減小鎖的粒度。
package com.bjsxt.base.sync006;
/**
* 使用synchronized代碼塊減小鎖的粒度,提高性能
*
*/
public class Optimize {
public void doLongTimeTask(){
try {
System.out.println("目前線程開始:" + Thread.currentThread().getName() +
", 正在執行一個較長時間的業務操作,其内容不需要同步");
Thread.sleep(2000);
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();
}
}
對象鎖
上述是一種使用目前對象加鎖的方式,事實上sync可以使用任意的object對象來加鎖。
/**
* 使用synchronized代碼塊加鎖,比較靈活
* @author alienware
*
*/
public class ObjectLock {
public void method1(){
synchronized (this) { //對象鎖
try {
System.out.println("do method1..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){ //類鎖
synchronized (ObjectLock.class) {
try {
System.out.println("do method2..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private Object lock = new Object();
public void method3(){ //任何對象鎖
synchronized (lock) {
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();
}
}
類鎖和對象鎖不在此過多贅述,任意對象鎖,要實作加鎖,必須是對同一個對象進行操作。雖然是三種類型的鎖的示範,看不出來鎖的優先級,看不出來有任何先後順序,就是簡單的示範。
String鎖的問題
需要注意的是避免使用string類型加鎖。string常量在常量池中隻有一個引用(相當于一把鎖被一直循環持有),是可以實作加鎖的效果的。但是如果new 一個string,就是一個對象,就可以都進來(因為是兩個對象鎖,不互相影響)。
/**
* synchronized代碼塊對字元串的鎖,注意String常量池的緩存功能
* @author alienware
*
*/
public class StringLock {
public void method() {
//new String("字元串常量")
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();
}
}
鎖改變的問題
要注意鎖改變的問題,如果用的字元串變量做的鎖,千萬不要修改這個字元串。如果修改了就起不到加鎖的作用了。
package com.bjsxt.base.sync006;
/**
* 鎖對象的改變問題
* @author alienware
*
*/
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("目前線程 : " + Thread.currentThread().getName() + "開始");
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();
}
}
死鎖
package com.bjsxt.base.sync006;
/**
* 死鎖問題,在設計程式時就應該避免雙方互相持有對方的鎖的情況
* @author alienware
*
*/
public class DeadLock implements Runnable{
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag){
this.tag = tag;
}
@Override
public void run() {
if(tag.equals("a")){
synchronized (lock1) {
try {
System.out.println("目前線程 : " + Thread.currentThread().getName() + " 進入lock1執行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("目前線程 : " + Thread.currentThread().getName() + " 進入lock2執行");
}
}
}
if(tag.equals("b")){
synchronized (lock2) {
try {
System.out.println("目前線程 : " + Thread.currentThread().getName() + " 進入lock2執行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("目前線程 : " + Thread.currentThread().getName() + " 進入lock1執行");
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
d1.setTag("a");
DeadLock d2 = new DeadLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}