併發編程總結之synchronized細節問題

摘要:本節主要介紹了併發編程下怎麼避免數據髒讀和什麼是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();
    }
}
  1. 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 結束

 

本文爲轉載博客,作者爲許進,原文鏈接爲http://xujin.org/bf/bf-synchronized/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章