目錄
1:synchronized簡介
2:synchronized使用方式
3:synchronized可重入性
4:synchronized可見性
5:synchronized實作原理
6:synchronized的缺陷
7:synchronized的使用注意
1:synchronized簡介
在多線程環境中對同一資源同時操作可能會導緻結果的不确定性。java内置了synchronized關鍵字來解決這種問題。synchronized通常也被稱為重量級鎖,盡管從JDK1.5新增了Lock,但synchronized憑借java自帶的高封裝性依舊被廣泛的使用。synchronized可以把任何一個非null的對象作為鎖。synchronized有兩種主要用法:第一種是對象鎖,包括方法鎖(預設鎖對象為this目前的執行個體對象)和同步代碼塊鎖(自己指定的鎖對象)。第二種是類鎖,指synchronized修飾靜态的方法或指定鎖為Class對象。
2:synchronized使用方式
首先,我先寫一段代碼
public class SynchronizedMethod1 implements Runnable {
static SynchronizedMethod1 synchronizedMethod = new SynchronizedMethod1();
static int i = 0;
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(synchronizedMethod);
Thread t2 = new Thread(synchronizedMethod);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
}
這段代碼大家應該可以看出來是有問題的,沒有保證對共享變量i的原子操作,導緻的結果就是:無法确定i輸出的值。
下面來示範synchronized的幾種用法,如何保證資料的準确性。
方式一: 同步代碼塊
public class SynchronizedMethod2 implements Runnable {
static SynchronizedMethod2 instance = new SynchronizedMethod2();
static int i = 0;
@Override
public void run() {
synchronized (this){
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
}
this指的是目前的執行個體對象。
方式二: 同步方法
public class SynchronizedMethod3 implements Runnable {
static SynchronizedMethod3 instance = new SynchronizedMethod3();
static int i = 0;
@Override
public void run() {
add();
}
public synchronized void add() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
}
第二種方式也能夠保證資料的準确。
下面我再對上述代碼進行改動。執行個體化兩個對象,分别給線程使用。這時會發現,上述的兩種方式都盡管都使用了synchronized,但依然無法保證資料的準确。原因是因為我們使用的是synchronized的對象鎖,對于不同的執行個體對象,線程隻能夠對自己引用的對象進行加鎖。
public class SynchronizedMethod3 implements Runnable {
static SynchronizedMethod3 instance1 = new SynchronizedMethod3();
static SynchronizedMethod3 instance2 = new SynchronizedMethod3();
static int i = 0;
@Override
public void run() {
add();
}
public synchronized void add() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
}
示範下面用法的時候先講一個概念:Java類可能有很多個對象,但隻有1個Class對象。這個很重要,要考的!!!比如我new一個Student對象A,一個Student對象B,這是兩個對象,但它們隻有一個Class對象,這個Class對象由JVM實作。
方式三: 靜态方法鎖
與第二種方式相比就是在同步方法上多加了static
public class SynchronizedMethod4 implements Runnable{
static SynchronizedMethod4 instance1 = new SynchronizedMethod4();
static SynchronizedMethod4 instance2 = new SynchronizedMethod4();
static int i = 0;
@Override
public void run() {
try {
add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void add() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
for (int j = 0; j < 100000; j++) {
i++;
}
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
當作用在靜态方法時鎖住的便是對象對應的Class執行個體,因為Class資料存在于永久帶,是以靜态方法鎖相當于該類的一個全局鎖。是以就算new了多個對象,但一樣能夠鎖住。
方式四: synchronized鎖為Class對象
public class SynchronizedMethod5 implements Runnable {
static SynchronizedMethod5 instance1 = new SynchronizedMethod5();
static SynchronizedMethod5 instance2 = new SynchronizedMethod5();
static int i = 0;
@Override
public void run() {
synchronized (SynchronizedMethod5.class) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終的結果是:" + i);
}
}
在方法四中,我們的鎖對象是*.class,無論執行個體化多少對象,這些對象也隻有一個class對象,是以,這種方式也是可以鎖住的。
3:synchronized可重入性
synchronized和Lock(ReentrantLock)都是可重入鎖,可重入鎖又叫做遞歸鎖。在這裡隻說明synchronized的可重入性,不對二者進行比較。
可重入性定義:同一線程在外層函數擷取到鎖後,内層函數可以直接再次擷取到鎖。
synchronized的可重入性有兩點好處:避免死鎖,提升封裝性。
這是什麼意思呢?我們用如下代碼來說明:
public class SynchronizedMethod6 {
static SynchronizedMethod6 instance = new SynchronizedMethod6();
public void method1() {
synchronized (SynchronizedMethod6.class) {
System.out.println("method1執行成功");
method2();
}
}
public void method2() {
synchronized (SynchronizedMethod6.class) {
System.out.println("method2執行成功");
}
}
public static void main(String[] args) {
instance.method1();
}
}
我們假設synchronized不具有重入性,在調用method2時,由于目前持有的鎖沒有釋放,又無法擷取到method2中的鎖,這時就會導緻死鎖。
由于synchronized是java内置的鎖,自帶重入性,是以封裝性更強。
原理:加鎖次數計數器。
每個對象都有一把鎖,JVM負責跟蹤對象被加鎖的次數。線程第一次擷取對象鎖的時候,計數變為1.當這個線程在此對象上再次獲得鎖的時候,計數會遞增。當任務離開的時候,計數遞減,當計數遞減為0的時候,鎖完全被釋放。
4:synchronized可見性
既然說到線程之間的可見性,那麼必須要先了解java的記憶體模型JMM(這篇部落格中簡單簡單講解了一下java的記憶體模型:https://blog.csdn.net/love1793912554/article/details/88618453)。在這裡不再詳細講述JMM。對于synchronized來說,在釋放鎖的時候,線程會把操作的資料重新整理到主記憶體中去,由此就可以保證線程之間的資料可見性。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxCMVRlT5VEVPp3a65EeV1GZ2hnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL1MTOwETOyETMxIDNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
5:synchronized實作原理
synchronized是java内置的關鍵字,由此可見它的重要性。但它無法直接通過源碼來分析。下面我用反編譯位元組碼資訊來分析一下synchronized的實作原理。
先用javac将方法一中的.java檔案編譯成.class檔案,執行javap -v SynchronizedMethod1.class,javap是jdk自帶的工具,想要仔細了解可以參考(https://blog.csdn.net/junsure2012/article/details/7099222)。
執行同步代碼塊需要先擷取對象的螢幕monitor。monitorenter對應多個monitorexit,釋放鎖的情況可能有多種,正常釋放鎖,異常釋放鎖,是以monitorexit相比較于monitorenter會多。
6:synchronized的缺陷
鎖的釋放情況少。synchronized釋放鎖隻有兩種情況,一種是正常流程運作結束,另一種是發生了異常。如果說一個線程正在執行IO操作(當然,不建議在鎖中進行耗時的IO操作,隻是舉例),那麼另一個線程就隻能焦急的等待。
試圖獲得鎖時不能設定逾時時間。排隊擷取鎖的線程會一直存在,不會因為暫時的堵塞而撤退。與之對應的tryLock(long time, TimeUnit unit) 在指定的時間内不能獲得鎖就會主動放棄。
不能中斷一個正在試圖獲得鎖的線程。
不夠靈活:每個鎖僅有單一的條件,加鎖和釋放鎖的時機單一。
無法知道是否成功擷取到鎖。
7:synchronized的使用注意
1:鎖的對象不能為空,鎖是存在于對象頭中的,對象都沒有,如何加鎖。
2:鎖的作用域不宜過大,簡單點說就是加鎖的代碼塊不能過多,如果代碼塊都存在與鎖中,代 碼的執行就變成了串行執行。這就無法展現多線程的效率了。
3:避免死鎖。在一個線程中避免擷取不同的鎖。
記錄自己的學習和成長,如果寫的有什麼不對的地方,還請大家多多指正。