synchronized 怎麼使用

文章目錄

  • 前言
  • 通過一系列的例子,瞭解synchronized 使用
  • 總結

前言

上一篇瞭解了synchronized,但是呢光懂理論沒用,關鍵是要會用,用demo的形式寫一下各種使用場景,這麼一來,就會對synchronized的使用更加透徹。

通過一系列的例子,瞭解synchronized 使用

1、synchronized 都會在哪些地方使用?

修飾一個代碼塊,作用的對象是調用這個代碼塊的對象。
修飾一個方法,作用的對象是調用這個方法的對象。
修飾一個靜態方法,作用是這個類的所有對象。
修飾一個類,作用的是這個類的所有對象

2、怎麼使用同步代碼塊?

兩個線程訪問同一個對象代碼塊,只有一個線程執行,另外一個線程被阻塞。
比如:

class MyRunnable implements Runnable{
  private static int count;//定義一個變量count;
  public MyRunnable(){
    count = 0;
  }
  @Override public void run() {
    synchronized (this){//同步代碼塊,鎖住的是MyRunnable這個實例對象
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName() + ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }

    }
  }
  public int getCount(){
    return count;
  }
}

輸出的日誌如下
2022-12-31 14:36:03.266 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 14:36:03.366 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 14:36:03.467 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 14:36:03.567 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 14:36:03.667 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 14:36:03.768 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 14:36:03.868 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 14:36:03.968 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 14:36:04.068 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 14:36:04.169 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:9

可以看到先執行線程1,再執行線程2,達到了同步目的。

2.1 這個時候,我們將執行的對象換一下

比如:

    MyRunnable myRunnable = new MyRunnable();
    MyRunnable myRunnable2 = new MyRunnable();
    Thread thread1 = new Thread(myRunnable, "線程1");
    Thread thread2 = new Thread(myRunnable2, "線程2");
    thread1.start();
    thread2.start();

執行結果:
2022-12-31 14:42:33.957 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 14:42:33.957 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:1
2022-12-31 14:42:34.057 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 14:42:34.057 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:2
2022-12-31 14:42:34.157 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 14:42:34.157 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:3
2022-12-31 14:42:34.257 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 14:42:34.257 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:4
2022-12-31 14:42:34.358 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 14:42:34.358 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:5

可以看到線程1和線程2,變成隨機的執行代碼塊,爲什麼呢?
因爲他們作用的不是同一個對象,線程1 執行的是 myRunnable對象,
而線程2 執行的是 myRunnable2對象。所以他們互不干擾,兩個線程就能同時執行。

2.2 當一個線程訪問一個對象的 synchronized(this) 代碼塊的時候,另外一個線程還是可以訪問, 該對象的沒有被synchronized(this) 修飾的代碼塊。

比如:

class Counter implements Runnable{
  private int count;
  public Counter(){
    count = 0;
  }

  /**
   * 對count 執行自增操作
   * */
  public void countAdd(){
    synchronized (this){
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName()+ ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
  //沒有對count 執行自增操作,只是打印
  public void printCount(){
    for(int i = 0; i < 5; i++){
      try {
        System.out.println(Thread.currentThread().getName() + "  count:" + count);
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  @Override public void run() {
    String threadName = Thread.currentThread().getName();
    if ("線程A".equals(threadName)){//線程A 要執行自增操作
       countAdd();
    }else if("線程B".equals(threadName)){
       printCount();
    }
  }
}

使用:
    Counter counter = new Counter();
    Thread threadA = new Thread(counter, "線程A");
    Thread threadB = new Thread(counter, "線程B");
    threadA.start();
    threadB.start();

結果:
2022-12-31 15:03:52.249 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:0
2022-12-31 15:03:52.249 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B  count:1
2022-12-31 15:03:52.349 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B  count:1
2022-12-31 15:03:52.349 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:1
2022-12-31 15:03:52.449 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:2
2022-12-31 15:03:52.449 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B  count:2
2022-12-31 15:03:52.549 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:3
2022-12-31 15:03:52.549 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B  count:3
2022-12-31 15:03:52.650 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:4
2022-12-31 15:03:52.650 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B  count:5

可以看到線程A 和 線程B 他們是互不影響的,線程B還是隨機執行的。
也就是說一個線程 在執行同一個對象的 synchronized(this)的代碼塊時候,
另外一個線程可以執行該對象的沒有被synchronized(this)修飾的代碼塊,不會阻塞。

3、怎麼給一個對象加鎖呢?
class Account {
  String name;
  float amount;

  public Account(String name, float amount){
    this.name = name;
    this.amount = amount;
  }

  /**
   * 存錢
   * */
  public void save(float money){
    amount += money;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * 取錢
   * */
  public void out(float money){
    amount -= money;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }


  public float getAmount(){
    return amount;
  }
}

class AccountOperator implements Runnable{
  private Account account;
  public AccountOperator(Account account){
    this.account = account;
  }
  @Override public void run() {
    synchronized (account){//對這個對象進行加鎖操作
      account.save(500);//存錢500;
      account.out(500);//取錢500;
      System.out.println(Thread.currentThread().getName() + ":" + account.amount);
    }
  }
}

使用:
    Account account = new Account("ssz", 10000.0f);
    AccountOperator accountOperator = new AccountOperator(account);
    final int THREAD_NUM = 10;
    //創建5個線程去隨機執行。
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++){
      threads[i] = new Thread(accountOperator, "Thread" + i);
      threads[i].start();
    }



結果:
2022-12-31 15:31:09.675 11884-11927/com.ssz.mvvmdemo I/System.out: Thread0:10000.0
2022-12-31 15:31:09.875 11884-11928/com.ssz.mvvmdemo I/System.out: Thread1:10000.0
2022-12-31 15:31:10.076 11884-11930/com.ssz.mvvmdemo I/System.out: Thread2:10000.0
2022-12-31 15:31:10.276 11884-11931/com.ssz.mvvmdemo I/System.out: Thread3:10000.0
2022-12-31 15:31:10.477 11884-11934/com.ssz.mvvmdemo I/System.out: Thread5:10000.0
2022-12-31 15:31:10.677 11884-11932/com.ssz.mvvmdemo I/System.out: Thread4:10000.0
2022-12-31 15:31:10.878 11884-11936/com.ssz.mvvmdemo I/System.out: Thread7:10000.0
2022-12-31 15:31:11.078 11884-11937/com.ssz.mvvmdemo I/System.out: Thread8:10000.0
2022-12-31 15:31:11.279 11884-11938/com.ssz.mvvmdemo I/System.out: Thread9:10000.0
2022-12-31 15:31:11.480 11884-11935/com.ssz.mvvmdemo I/System.out: Thread6:10000.0

可以看到線程是隨機的執行,但是呢,對於存錢取錢的操作仍然是不受影響的,因爲這塊是進行了同步存取操作,是對Account進行了加鎖操作,保證了不受其他線程的干擾。

3.1 假如沒有明確的對象作爲鎖,只想同步一段代碼塊怎麼辦呢?
class Test implements Runnable{
  private byte[] lock = new byte[0]//一個特殊的實例對象

  public void test(){
    synchronized(lock){
        //todo 要同步的代碼
    }
  }

  public void run(){
  }

}

爲什麼使用byte[] 作爲對象呢?
因爲byte 數組創建起來比任何對象都經濟。
跟Object object = new Object() 比起來的話,查看編譯後的字節碼發現。
byte 數組 只需要3行操作碼,而 object 需要7行操作碼

3.2 synchronized 在修飾方法的時候能不能繼承呢?

synchronized 是不能繼承的,也就是如果子類要同步,
要嘛調用父類的同步方法,要嘛就是在方法前,添加一個synchronized關鍵字。
比如:

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }//調用父類的同步方法
}

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { } //添加關鍵字
}

4、怎麼修飾一個靜態方法呢?

當我們在修飾靜態方法的時候,因爲靜態方法是屬於類的,所以鎖住的是類的所有對象。
比如:

class NewRunnable implements Runnable{
  private static int count;
  public NewRunnable(){
    count = 0;
  }

  public synchronized static void method(){  //修飾的是一個靜態方法
    for (int i = 0; i < 5; i++){
      try {
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  @Override public void run() {
    method();
  }
}

使用:
    NewRunnable newRunnable = new NewRunnable();
    NewRunnable newRunnable2 = new NewRunnable();
    Thread thread1 = new Thread(newRunnable, "線程1"); //newRunnable
    Thread thread2 = new Thread(newRunnable2, "線程2");//newRunnable2
    thread1.start();
    thread2.start();

結果:
2022-12-31 17:02:21.483 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 17:02:21.583 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 17:02:21.683 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 17:02:21.783 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 17:02:21.884 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 17:02:21.984 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 17:02:22.084 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 17:02:22.184 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 17:02:22.285 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 17:02:22.385 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:9

可以看到,雖然是不同的對象,但是呢,卻還是按順序執行了。最主要是同步的是靜態方法,而靜態方法是屬於類的,這相當於鎖住了這個類,所以,就能阻止其他線程調用,只能等待第一個線程執行完,再執行第二個線程。

4.1 怎麼修飾一個類呢?

我們對上面修飾靜態方法做個改造,就是把synchronized 放到方法裏頭,然後使用 synchronized (NewRunnable.class)
比如:

class NewRunnable implements Runnable{
  private static int count;
  public NewRunnable(){
    count = 0;
  }

  public void method(){    //不是靜態方法,就是普通方法
    synchronized (NewRunnable.class){ //鎖的作用對象是類
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName() + ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }

  @Override public void run() {
    method();
  }
}
結果情況1:
2022-12-31 17:10:52.476 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:0
2022-12-31 17:10:52.577 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:1
2022-12-31 17:10:52.677 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:2
2022-12-31 17:10:52.778 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:3
2022-12-31 17:10:52.878 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:4
2022-12-31 17:10:52.978 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:5
2022-12-31 17:10:53.078 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:6
2022-12-31 17:10:53.179 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:7
2022-12-31 17:10:53.279 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:8
2022-12-31 17:10:53.379 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:9

結果情況2:
2022-12-31 17:12:17.787 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 17:12:17.887 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 17:12:17.987 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 17:12:18.087 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 17:12:18.187 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 17:12:18.288 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 17:12:18.388 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 17:12:18.489 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 17:12:18.589 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 17:12:18.689 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:9

我們看到可能先執行的線程1,也可能先執行的線程2,但是不管是誰開始執行,只要誰先開始,另外一個線程就得等着。
所以,這就保證了同步。
這裏是給這個類加鎖,所以他們都是共用1把鎖,只有一方把鎖釋放,另外一方纔能執行。

5 使用synchronized 有哪些注意點呢?

1、定義接口的時候,不能使用synchronized 關鍵字。
2、構造方法不能使用synchronized 關鍵字,但可以使用synchronized來同步代碼塊。

總結

總的來講:
1、synchronized 關鍵字用在非靜態的方法上,或者對象上,所獲得的鎖是針對對象的;
如果是一個靜態方法,或者一個類,那麼鎖是針對類的所有對象的。
2、每個對象只有一把鎖與之關聯,需要等一方釋放,另一方纔能執行。
3、使用同步是需要系統很大開銷的,所以非必要情況下不要進行同步鎖操作。

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