溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JAVA面試題 簡談你對synchronized關鍵字的理解

發布時間:2020-09-01 11:24:48 來源:腳本之家 閱讀:146 作者:Java螞蟻 欄目:編程語言

面試官:sychronized關鍵字有哪些特性?

應聘者:

  • 可以用來修飾方法;
  • 可以用來修飾代碼塊;
  • 可以用來修飾靜態方法;
  • 可以保證線程安全;
  • 支持鎖的重入;
  • sychronized使用不當導致死鎖;

了解sychronized之前,我們先來看一下幾個常見的概念:內置鎖、互斥鎖、對象鎖和類鎖。

內置鎖

在Java中每一個對象都可以作為同步的鎖,那么這些鎖就被稱為內置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。

互斥鎖

內置鎖同時也是一個互斥鎖,這就是意味著最多只有一個線程能夠獲得該鎖,當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,直到線程B拋出異?;蛘哒绦型戤呩尫胚@個鎖;如果B線程不釋放這個鎖,那么A線程將永遠等待下去。

對象鎖和類鎖

對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,但是,兩個鎖實際是有很大的區別的。

  • 對象鎖是用于對象實例方法;
  • 類鎖是用于類的靜態方法或者一個類的class對象上的

一個對象無論有多少個同步方法區,它們共用一把鎖,某一時刻某個線程已經進入到某個synchronzed方法,那么在該方法沒有執行完畢前,其他線程無法訪問該對象的任何synchronzied 方法的,但可以訪問非synchronzied方法。

如果synchronized方法是static的,那么當線程訪問該方法時,它鎖的并不是synchronized方法所在的對象,而是synchronized方法所在對象的對應的Class對象,

因為java中無論一個類有多少個對象,這些對象會對應唯一一個Class對象,因此當線程分別訪問同一個類的兩個對象的static,synchronized方法時,他們的執行也是按順序來的,也就是說一個線程先執行,一個線程后執行。

synchronized的用法:修飾方法和修飾代碼塊,下面分別分析這兩種用法在對象鎖和類鎖上的效果。

對象鎖的synchronized修飾方法和代碼塊

public class TestSynchronized {
  public void test1() {
    synchronized (this) {
      int i = 5;
      while (i-- > 0) {
        System.out.println(Thread.currentThread().getName() + " : " + i);
        try {
          Thread.sleep(500);
        } catch (InterruptedException ie) {
        }
      }
    }
  }
 
  public synchronized void test2() {
    int i = 5;
    while (i-- > 0) {
      System.out.println(Thread.currentThread().getName() + " : " + i);
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
      }
    }
  }
 
  public static void main(String[] args) {
    final TestSynchronized myt2 = new TestSynchronized();
    Thread test1 = new Thread(new Runnable() {
      public void run() {
        myt2.test1();
      }
    }, "test1");
    Thread test2 = new Thread(new Runnable() {
      public void run() {
        myt2.test2();
      }
    }, "test2");
    test1.start();
    test2.start();
  }
}

打印結果如下:

test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0

上述的代碼,第一個方法用了同步代碼塊的方式進行同步,傳入的對象實例是this,表明是當前對象;第二個方法是修飾方法的方式進行同步

。因為第一個同步代碼塊傳入的this,所以兩個同步代碼所需要獲得的對象鎖都是同一個對象鎖,下面main方法時分別開啟兩個線程,分別調用test1和test2方法,那么兩個線程都需要獲得該對象鎖,另一個線程必須等待。

上面也給出了運行的結果可以看到:直到test2線程執行完畢,釋放掉鎖,test1線程才開始執行。這里test2方法先搶到CPU資源,故它先執行,它獲得了鎖,它執行完畢后,test1才開始執行。

如果我們把test2方法的synchronized關鍵字去掉,執行結果會如何呢? 

test1 : 4
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 3
test1 : 2
test1 : 1
test1 : 0

我們可以看到,結果輸出是交替著進行輸出的,這是因為,某個線程得到了對象鎖,但是另一個線程還是可以訪問沒有進行同步的方法或者代碼。進行了同步的方法(加鎖方法)和沒有進行同步的方法(普通方法)是互不影響的,一個線程進入了同步方法,得到了對象鎖,其他線程還是可以訪問那些沒有同步的方法(普通方法)。

類鎖的修飾(靜態)方法和代碼塊  

public class TestSynchronized {
  public void test1() {
    synchronized (TestSynchronized.class) {
      int i = 5;
      while (i-- > 0) {
        System.out.println(Thread.currentThread().getName() + " : " + i);
        try {
          Thread.sleep(500);
        } catch (InterruptedException ie) {
        }
      }
    }
  }
 
  public static synchronized void test2() {
    int i = 5;
    while (i-- > 0) {
      System.out.println(Thread.currentThread().getName() + " : " + i);
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
      }
    }
  }
 
  public static void main(String[] args) {
    final TestSynchronized myt2 = new TestSynchronized();
    Thread test1 = new Thread(new Runnable() {
      public void run() {
        myt2.test1();
      }
    }, "test1");
    Thread test2 = new Thread(new Runnable() {
      public void run() {
        TestSynchronized.test2();
      }
    }, "test2");
    test1.start();
    test2.start();
  }
}

輸出結果如下:

test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

類鎖修飾方法和代碼塊的效果和對象鎖是一樣的,因為類鎖只是一個抽象出來的概念,只是為了區別靜態方法的特點,因為靜態方法是所有對象實例共用的,所以對應著synchronized修飾的靜態方法的鎖也是唯一的,所以抽象出來個類鎖。其實這里的重點在下面這塊代碼,synchronized同時修飾靜態和非靜態方法

public class TestSynchronized {
  public synchronized void test1() {
    int i = 5;
    while (i-- > 0) {
      System.out.println(Thread.currentThread().getName() + " : " + i);
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
      }
    }
  }
 
  public static synchronized void test2() {
    int i = 5;
    while (i-- > 0) {
      System.out.println(Thread.currentThread().getName() + " : " + i);
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
      }
    }
  }
 
  public static void main(String[] args) {
    final TestSynchronized myt2 = new TestSynchronized();
    Thread test1 = new Thread(new Runnable() {
      public void run() {
        myt2.test1();
      }
    }, "test1");
    Thread test2 = new Thread(new Runnable() {
      public void run() {
        TestSynchronized.test2();
      }
    }, "test2");
    test1.start();
    test2.start();
  }
}

輸出結果如下:

test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面代碼synchronized同時修飾靜態方法和實例方法,但是運行結果是交替進行的,這證明了類鎖和對象鎖是兩個不一樣的鎖,控制著不同的區域,它們是互不干擾的。同樣,線程獲得對象鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。

synchronized是如何保證線程安全的

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,演示線程的安全問題:

我們來模擬一下火車站賣票過程,總共有100張票,總共有三個窗口賣票。

public class SellTicket {
  public static void main(String[] args) {
    // 創建票對象
    Ticket ticket = new Ticket();
    // 創建3個窗口
    Thread t1 = new Thread(ticket, "窗口1");
    Thread t2 = new Thread(ticket, "窗口2");
    Thread t3 = new Thread(ticket, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}
 
// 模擬票
class Ticket implements Runnable {
  // 共100票
  int ticket = 100;
 
  @Override
  public void run() {
    // 模擬賣票
    while (true) {
      if (ticket > 0) {
        // 模擬選坐的操作
        try {
          Thread.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在賣票:"
            + ticket--);
      }
    }
  }
}

運行結果發現:上面程序出現了問題

  • 票出現了重復的票
  • 錯誤的票 0、-1

其實,線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

那么出現了上述問題,我們應該如何解決呢?

線程同步(線程安全處理Synchronized)

java中提供了線程同步機制,它能夠解決上述的線程安全問題。

線程同步的方式有兩種:

  • 方式1:同步代碼塊
  • 方式2:同步方法

同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {
  可能會產生線程安全問題的代碼
}

同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。

使用同步代碼塊,對火車站賣票案例中Ticket類進行如下代碼修改:

public class SellTicket {
  public static void main(String[] args) {
    // 創建票對象
    Ticket ticket = new Ticket();
    // 創建3個窗口
    Thread t1 = new Thread(ticket, "窗口1");
    Thread t2 = new Thread(ticket, "窗口2");
    Thread t3 = new Thread(ticket, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}
 
// 模擬票
class Ticket implements Runnable {
  // 共100票
  int ticket = 100;
 
  Object lock = new Object();
 
  @Override
  public void run() {
    // 模擬賣票
    while (true) {
      // 同步代碼塊
      synchronized (lock) {
        if (ticket > 0) {
          // 模擬選坐的操作
          try {
            Thread.sleep(1);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName()
              + "正在賣票:" + ticket--);
        }
      }
    }
  }
}

當使用了同步代碼塊后,上述的線程的安全問題,解決了。

同步方法

同步方法:在方法聲明上加上synchronized

public synchronized void method(){
    可能會產生線程安全問題的代碼
}

同步方法中的鎖對象是 this

使用同步方法,對火車站賣票案例中Ticket類進行如下代碼修改:

public class SellTicket {
  public static void main(String[] args) {
    // 創建票對象
    Ticket ticket = new Ticket();
    // 創建3個窗口
    Thread t1 = new Thread(ticket, "窗口1");
    Thread t2 = new Thread(ticket, "窗口2");
    Thread t3 = new Thread(ticket, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}
 
// 模擬票
class Ticket implements Runnable {
  // 共100票
  int ticket = 100;
 
  Object lock = new Object();
 
  @Override
  public void run() {
    // 模擬賣票
    while (true) {
      // 同步方法
      method();
    }
  }
 
  // 同步方法,鎖對象this
  public synchronized void method() {
    if (ticket > 0) {
      // 模擬選坐的操作
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "正在賣票:"
          + ticket--);
    }
  }
}

synchronized支持鎖的重入嗎?  

我們先來看下面一段代碼:

public class ReentrantLockDemo {
  public synchronized void a() {
    System.out.println("a");
    b();
  }
 
  private synchronized void b() {
    System.out.println("b");
  }
 
  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        ReentrantLockDemo d = new ReentrantLockDemo();
        d.a();
      }
    }).start();
  }
}

上述的代碼,我們分析一下,兩個方法,方法a和方法b都被synchronized關鍵字修飾,鎖對象是當前對象實例,按照上文我們對synchronized的了解,如果調用方法a,在方法a還沒有執行完之前,我們是不能執行方法b的,方法a必須先釋放鎖,方法b才能執行,方法b處于等待狀態,那樣不就形成死鎖了嗎?那么事實真的如分析一致嗎?

運行結果發現:

a
b

代碼很快就執行完了,實驗結果與分析不一致,這就引入了另外一個概念:重入鎖。在 java 內部,同一線程在調用自己類中其他 synchronized 方法/塊或調用父類的 synchronized 方法/塊都不會阻礙該線程的執行。就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。在JDK1.5后對synchronized關鍵字做了相關優化。

synchronized死鎖問題

同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程序出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){
  synchronized(B鎖){
  }
}

我們進行下死鎖情況的代碼演示:

public class DeadLock {
  Object obj1 = new Object();
  Object obj2 = new Object();
 
  public void a() {
    synchronized (obj1) {
      synchronized (obj2) {
        System.out.println("a");
      }
    }
  }
 
  public void b() {
    synchronized (obj2) {
      synchronized (obj1) {
        System.out.println("b");
      }
    }
  }
 
  public static void main(String[] args) {
    DeadLock d = new DeadLock();
    new Thread(new Runnable() {
      @Override
      public void run() {
        d.a();
      }
    }).start();
 
    new Thread(new Runnable() {
      @Override
      public void run() {
        d.b();
      }
    }).start();
  }
}

上述的代碼,我們分析一下,兩個方法,我們假設兩個線程T1,T2,T1運行到方法a了,拿到了obj1這把鎖,此時T2運行到方法b了,拿到了obj2這把鎖,T1要往下執行,就必須等待T2釋放了obj2這把鎖,線程T2要往下面執行,就必須等待T1釋放了持有的obj1這把鎖,他們兩個互相等待,就形成了死鎖。

為了演示的更明白,需要讓兩個方法執行過程中睡眠10ms,要不然很難看到現象,因為計算機執行速度賊快

public class DeadLock {
  Object obj1 = new Object();
  Object obj2 = new Object();
 
  public void a() {
    synchronized (obj1) {
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (obj2) {
        System.out.println("a");
      }
    }
  }
 
  public void b() {
    synchronized (obj2) {
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (obj1) {
        System.out.println("b");
      }
    }
  }
 
  public static void main(String[] args) {
    DeadLock d = new DeadLock();
    new Thread(new Runnable() {
      @Override
      public void run() {
        d.a();
      }
    }).start();
 
    new Thread(new Runnable() {
      @Override
      public void run() {
        d.b();
      }
    }).start();
  }
 
}

感興趣的童鞋,下去可以試一下,程序執行不完,永遠處于等待狀態。

總結

  • sychronized是隱式鎖,是JVM底層支持的關鍵字,由JVM來維護;
  • 單體應用下,多線程并發操作時,使用sychronized關鍵字可以保證線程安全;
  • sychronized可以用來修飾方法和代碼塊,此時鎖是當前對象實例,修飾靜態方法時,鎖是對象的class字節碼文件;
  • 一個線程進入了sychronized修飾的同步方法,得到了對象鎖,其他線程還是可以訪問那些沒有同步的方法(普通方法);
  • sychronized支持鎖的重入;

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女