一、簡介
Synchronized一句話來解釋其作用就是:能夠保證同一時刻最多隻有一個線程執行該段代碼,以達到併發安全的效果。也就是說Synchronized就好比是一把鎖,某個線程把資源鎖住了之後,別人就不能使用了,只有當這個線程用完了別人才能用。
對於Synchronized關鍵字來說,它是併發編程中一個元老級角色,也就是說你只要學習併發編程,就必須要學習Synchronized關鍵字。由此可見其地位。
說了這麼多,好像我們還沒體驗過它的威力。我們就直接舉個例子,來分析一下。
public class SynTest01 implements Runnable{ static int a=0; public static void main(String[] args) throws InterruptedException { SynTest01 syn= new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start();thread1.join(); thread2.start();thread2.join(); System.out.println(a); } @Override public void run() { for(int i=0;i<1000;i++) { a++; } } }
上面代碼要完成的功能就是,thread1對a進行增加,一直到1000,thread2再對a進行增加,一直到2000。不過如果我們運行過之後我們就會發現,最後的輸出值總是小於2000,這是為什麼呢?
這是因為我們在執行a++的時候其實包含了以下三個操作:
(1)線程1讀取a
(2)線程1將a加1
(3)將a的值寫入內存
出錯原因的關鍵就在於第二操作和第三個操作之間,此時線程1還沒來得及把a的值寫入內存,線程2就把舊值讀走了,這也就造成了a加了兩次,但是內存中的a的值只增加了1。這也就是不同步現象。
但是如果說我們使用了Synchronized關鍵字之後呢?
public class SynTest01 implements Runnable{ static int a=0; Object object = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn= new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start();thread1.join(); thread2.start();thread2.join(); System.out.println(a); } @Override public void run() { synchronized (object) { for(int i=0;i<1000;i++) { a++; } }//結束 } }
現在我們使用synchronized關鍵字把這一塊代碼鎖住,不管你怎麼輸出都是2000了,鎖住之後,同一時刻只有一個線程進入。也就不會發生上面a寫操作不同步的現象了。
現在相信你開始覺得synchronized關鍵字的確很實用,可以解決多線程中的很多問題。上面這個小例子只是帶我們去簡單的認識一下,下面我們就來看看其詳細的使用。
二、使用
對於synchronized關鍵字來說,一共可以分為兩類:對象鎖和類鎖。
我們一個一個來看如何使用。
1、對象鎖
對於對象鎖來說,又可以分為兩個,一個是方法鎖,一個是同步代碼塊鎖。
(1)同步代碼塊鎖
同步代碼塊鎖主要是對代碼塊進行加鎖,其實已經演示過了,就是上面的那個案例。不過為了保持一致我們再舉一個例子。
public class SynTest01 implements Runnable { Object object = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn = new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); //線程1和線程2只要有一個還存活就一直執行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序運行結束"); } @Override public void run() { synchronized (object) { try { System.out.println(Thread.currentThread().getName() + "線程執行了run方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "執行2秒鐘之後完畢"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
在這個例子中,我們使用了synchronized鎖住了run方法中的代碼塊。表示同一時刻只有一個線程能夠進入代碼塊。就好比是去醫院掛號,前面一個人辦完了業務,下一個人才開始。
在這裡面我們看到,線程1和線程2使用的是同一個鎖,也就是我們new的Object。如果我們讓線程1和線程2每一個人擁有一個鎖對象呢?
public class SynTest01 implements Runnable { Object object1 = new Object(); Object object2 = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn = new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); //線程1和線程2只要有一個還存活就一直執行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序運行結束"); } @Override public void run() { synchronized (object1) { try { System.out.println(Thread.currentThread().getName() + "線程執行了object1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "執行object1完畢"); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (object2) { try { System.out.println(Thread.currentThread().getName() + "線程執行object2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "執行object2完畢"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
現在線程1和線程2每個人擁有一把鎖,去訪問不同的方法資源。這時候會出現什麼情況呢?
我們同樣用一張圖看一下其原理。
也就是說,相當於兩個業務有倆窗口都可以辦理,但是兩個任務都需要排隊辦理。
同步代碼塊鎖總結:
同步代碼塊鎖主要是對代碼塊進行加鎖,此時同一時刻只能有一個線程獲取到該資源,要注意每一把鎖只負責當前的代碼塊,其他的代碼塊不管。
以上就是同步代碼快的使用方法。下面我們看對象鎖的另外一種形式,那就是方法鎖。這裡的方法鎖指代的是普通方法。
(2)方法鎖
方法鎖相比較同步代碼塊鎖就簡單很多了,就是在普通方法上添加synchronized關鍵字修飾即可。
public class SynTest2 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest2 syn = new SynTest2(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); // 線程1和線程2只要有一個還存活就一直執行 while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("main程序運行結束"); } @Override public void run() { method(); } public synchronized void method() { try { System.out.println(Thread.currentThread().getName() + "進入到了方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "執行完畢"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在這個例子中我們使用兩個線程對同一個普通方法進行訪問,結果可想而知,也就是同一時刻只能有一個線程進入到此方法。我們運行一下,看一下結果。
跟我們預想的一樣,很簡單。不過我們想過一個問題沒有,此時我們synchronized關鍵字加了一把鎖,這個鎖指代是誰呢?像同步代碼塊鎖synchronized (object),這裡面都有object,但是方法鎖是誰呢?
答案就是this對象,也就是說我們在方法鎖裡面synchronized其實鎖的就是當前this對象。我們如何去驗證this鎖的存在呢?不如我們再舉一個例子:
public class SynTest3 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest3 syn = new SynTest3(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); // 線程1和線程2只要有一個還存活就一直執行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序運行結束"); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開方法1,並釋放鎖"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method2() { try { System.out.println(Thread.currentThread().getName() + "進入到了方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開方法2,並釋放鎖"); } catch (InterruptedException e) { e.printStackTrace(); } } }
上面這個例子中,我們定義了兩個synchronized關鍵字修飾的方法method1和method2,然後讓兩個線程同時運行,我們測試一下看看會出現什麼結果:
從結果來看,會發現不管是method1還是method2,同一個時刻兩個方法只能有一個線程在運行。這也就是this鎖導致的。我們再給一張圖描述一下其原理。
現在應該明白了吧,這也就驗證了方法鎖的存在。也驗證了方法鎖的原理。下面我們繼續。討論一下類鎖。
2、類鎖
上面的鎖都是對象鎖,下面我們看看類鎖。類鎖其實也有兩種形式,一種是static方法鎖,一種是class鎖。
(1)static方法鎖
在java中,java的類對象可能有無數個,但是類卻只有一個。首先我們看第一種形式。
public class SynTest4 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest4 instance1 = new SynTest4(); SynTest4 instance2 = new SynTest4(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); System.out.println("main程序運行結束"); } @Override public void run() { method1(); } public static synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "進入到了靜態方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開靜態方法,並釋放鎖"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在這個例子中我們定義了兩個不同的對象instance1和instance2。分別去執行了method1。會出現什麼結果呢?
如果我們把static關鍵字去掉,很明顯現在就是普通方法了,如果我們再去運行,由於instance1和instance2是兩個不同的對象,那麼也就是兩個不同的this鎖,這時候就能隨便進入了。我們去掉static關鍵字之後運行一下:
現在看到了,由於是兩個不同的this鎖,所以都能進入,就好比是一個門有兩把鑰匙,每一把都能打開門。
(2)class鎖
這種用法我們直接看例子再來分析一下:
public class SynTest5 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest5 instance1 = new SynTest5(); SynTest5 instance2 = new SynTest5(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); // 線程1和線程2只要有一個還存活就一直執行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序運行結束"); } @Override public void run() { method1(); } public void method1() { synchronized (SynTest5.class) { try { System.out.println(Thread.currentThread().getName() + "進入到了方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "離開方法"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
在這個例子中我們使用了同步代碼塊,不過synchronized關鍵字包裝的可不是object了,而是SynTest5.class。我們還定義了兩個不同的對象實例instance1和instance2。運行一下我們會發現,線程1和線程2依然會依次執行。
以上就是synchronized關鍵字的幾種常見的用法,到這裡我們來一個總結:
對於同步不同步,關鍵點在於鎖,兩個線程執行的是同一把鎖,那麼就依次排隊等候,兩個線程執行的不是同一把鎖,那就各幹各的事。
基本的使用我們也講完了,下面我們進入下一個專題,那就是我們需要注意的事項。這是面試常考的一個問題,不管是機試還是面試。