前言
在上一篇 多線程(一)、基礎概念及notify()和wait()的使用 文章中我們講了多線程的一些基礎概念還有等待通知機制,在講線程之間共享資源的時候,提到會出現數據不同步問題,我們先通過一個示例來演示這個問題。
/**
* @author : EvanZch
* description:
**/
public class SynchronizedTest {
// 賦count初始值爲0
public static int count = 0;
// 進行累加操作
public void add() {
count++;
}
public static class TestThread extends Thread {
private SynchronizedTest synchronizedTest;
public TestThread(SynchronizedTest synchronizedTest) {
this.synchronizedTest = synchronizedTest;
}
@Override
public void run() {
super.run();
// 執行10000次累加
for (int x = 0; x < 10000; x++) {
synchronizedTest.add();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
// 開啓兩個線程
new TestThread(synchronizedTest).start();
new TestThread(synchronizedTest).start();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
}
可以看到,我程序中我們啓動了兩個線程,同時對 Count
變量進行累加操作,每個線程循環累加10000次,我們預想的結果,獲取的count值應該會是20000,執行程序可以發現。
0?爲什麼結果會是0?因爲我們在main裏面開啓線程執行,方法是順序執行,當執行到 輸出語句的時候,線程run方法還沒有啓動,所以這裏打印的是count的初始值 0;
怎麼獲取到正確結果?
1、等待一會在獲取結果
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new TestThread(synchronizedTest).start();
new TestThread(synchronizedTest).start();
// 等待一秒再回去結果
Thread.sleep(1000);
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
我們在獲取結果之前,先等待一秒,結果如下:
結果不再爲 0 ,但是結果也不是我們預想的 20000啊,難道是等待時間不夠?我們增加等待時間,在運行,發現結果也不是20000,這麼看,使用等待時間不嚴謹,因爲沒辦法判斷線程執行結束時間(其實線程執行很快的,遠不需要幾秒),那我們可以使用 join方法。
2、thread.join()
我們先看一下 thread 的 join方法
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
註釋大概意思是:當調用join方法後,會進行阻塞,直到該線程任務執行結束。
可以讓線程順序執行。
那我們可以簡單修改代碼,讓兩個線程執行結束後再打印結果
這裏需要注意,我們是在 main 這個線程裏面調用 join 方法, 則兩個線程會在main 線程阻塞,但是兩個子線程還是在並行處理,都執行結束後纔會喚醒 main 線程執行後續操作。
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
TestThread testThread = new TestThread(synchronizedTest);
TestThread testThread1 = new TestThread(synchronizedTest);
testThread.start();
testThread1.start();
// 讓程序順序執行
testThread.join();
testThread1.join();
// 當兩個線程任務結束後再獲取結果
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
結果:
發現結果也不是我們預想的 20000,我們使用了 join()
方法,它會在調用線程進行阻塞(main),當testThread
和 testThread1
都執行結束後再喚醒調用線程 , 能確保兩個線程肯定是執行結束了的,可是結果跟預期不一致,多次打印,發現結果一直在 10000 ~ 20000 這個區間波動。
爲什麼會出現這種情況?
上一篇文章講過,同一個進程的多個線程共享該進程的所有資源,當多個線程同時訪問一個對象或者一個對象的成員變量,可能會導致數據不同步問題,比如 線程A 對數據a進行操作,需要從內存中進行讀取然後進行相應的操作,操作完成後再寫入內存中,但是如果數據還沒有寫入內存中的時候,線程B 也來對這個數據進行操作,取到的就是還未寫入內存的數據,導致前後數據同步問題,我們也叫線程不安全操作。
比如 線程 A 取到 count 的時候,其值爲 100,加 1 後再放入內存中,如果在放入內存之前 線程B 也來拿 count 並對其進行累加操作,這個時候 **線程B **取到的 count 值 還是100,加 1 後放入內存,這個時候值爲101, 這樣 線程 A 進行累加的那步操作就沒有被算上,這就是爲啥,最後兩個線程算出來的結果肯定是小於 20000。
怎麼避免這種情況?
我們知道出現這種情況的原因是操作的時候,因爲多個線程同時訪問一個對象或者對象的成員變量,要處理這個問題,我們就引入了關鍵字 synchronized
正文
一、內置鎖 synchronized
關鍵字 synchronized
可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。
鎖又分爲對象鎖和類鎖:
對象鎖: 對一個對象實例進行鎖操作,實例對象可以有多個,不同對象實例的對象鎖互不干擾。
類鎖:用於類的靜態方法或者類的Class對象進行鎖操作。我們知道每個類只有一個Class對象,也就只有一個類鎖。
注意點:
類鎖只是一個概念上的東西,它鎖的也是對象,只不過這個對象是類的Class對象,其唯一存在。
類鎖和對象鎖之間互不干擾。
通過上面的案例,我們簡單改改,我們在執行累加方法上加上 synchronized
關鍵字,然後再運行。
/**
* @author : EvanZch
* description:
**/
public class SynchronizedTest {
public static int count = 0;
// 我們對add方法添加關鍵字 synchronized
public synchronized void add() {
count++;
}
public static class TestThread extends Thread {
private SynchronizedTest synchronizedTest;
public TestThread(SynchronizedTest synchronizedTest) {
this.synchronizedTest = synchronizedTest;
}
@Override
public void run() {
super.run();
for (int x = 0; x < 10000; x++) {
synchronizedTest.add();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
TestThread testThread = new TestThread(synchronizedTest);
TestThread testThread1 = new TestThread(synchronizedTest);
testThread.start();
testThread1.start();
// 讓程序順序執行
testThread.join();
testThread1.join();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
}
結果:
可以看到我們只加了一個關鍵字 synchronized
,結果就跟我們預期的 20000 一致,我們將 synchronized
添加到方法上,就確保了多個線程同一時刻只有一個線程對此方法進行操作,這樣就確保了線程安全問題。
前面說了內置鎖存在對象鎖和類鎖 ,我們來看一下具體怎麼實現和區別。
1.1、對象鎖
對一個對象實例進行鎖操作,實例對象可以有多個,不同對象實例的對象鎖互不干擾。
我們在前面的示例上進行更改。
方法鎖:
// 非靜態方法
public synchronized void add() {
count++;
}
同步代碼塊鎖:
public void add(){
synchronized (this){
count ++;
}
}
或者:
// 非靜態變量
public Object object = new Object();
public void add(){
synchronized (object){
count ++;
}
}
我們可以看到對象鎖都是對非靜態方法和非靜態變量進行加鎖,以上三種從本質上來說沒有區別,我們這個時候再改一下我們的示例代碼,來驗證一下 不同對象實例的對象鎖互不干擾。
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
// 我們再創建一個 SynchronizedTest 對象
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
// 傳入 synchronizedTest
TestThread testThread = new TestThread(synchronizedTest);
// 傳入 synchronizedTest1
TestThread testThread1 = new TestThread(synchronizedTest1);
testThread.start();
testThread1.start();
// 讓程序順序執行
testThread.join();
testThread1.join();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
我們開啓兩個線程,分別傳入了不同的實例對象,這個時候再多次運行,查看運行結果。
結果:
我們多次運行獲取結果,發現都獲取不到我們期望的20000,可以我們明明也在add()
方法上添加了 synchronized
啊,唯一不同的就是,兩個線程傳入了不同的對象,所以通過結果,我們可以得出,不同對象的對象鎖之間,是互不影響,各種運行。
1.2、類鎖
用於類的靜態方法或者類的Class對象進行鎖操作。我們知道每個類只有一個Class對象,也就只有一個類鎖。
類鎖其實也是對象鎖,只不過鎖的對象比較特殊。
靜態方法鎖:
// 靜態方法
public static synchronized void add() {
count++;
}
同步代碼塊鎖:
public void add(){
// 傳入Class對象
synchronized (SynchronizedTest.class){
count ++;
}
}
或者:
// 靜態成員變量
public static Object object = new Object();
public void add(){
synchronized (object){
count ++;
}
}
我們知道靜態變量和類的Class對象在內存中只存在一個,所以我們對add
方法通過類鎖方式進行加鎖,不管外界這個時候傳的對象有多少個,它也是唯一的,我們再執行上面的main方法,打印結果:
可以看到結果和期望一致。
知識拓展 :static 關鍵字和 new 一個對象,做了什麼操作?
static 關鍵字:
- 靜態變量是隨着類加載時被完成初始化的,它在內存中僅有一個,且 JVM 也只會爲它分配一次內存,同時類所有的實例都共享靜態變量,即一處變、處處變,可以直接通過類名來訪問它。
- 但是實例變量則不同,它是伴隨着new實例化的,每創建一個實例就會產生一個實例變量,它與該實例同生共死。
new 一個對象,底層做了啥?
1、Jvm加載未加載的字節碼,開闢空間
2、靜態初始化(1靜態代碼塊和2靜態變量)
3、成員變量初始化(1普通代碼塊和2普通成員變量)
4、構造器初始化(構造函數)