zl程序教程

您现在的位置是:首页 >  Java

当前栏目

「 代码性能优化 」作为一名Java程序员,你真的了解 synchronized 吗?(三)

2023-03-14 22:44:26 时间

前言

文接上篇,本文将继续介绍 Synchronized,感兴趣的小伙伴继续跟博主一起讨论下。

更多、更体系化的内容请持续关注博主,您的 关注、点赞、收藏 都将是小编持续创作的动力!

Synchronized关键字用法

前文中提到synchronized 的用法可以从两个维度上面分类,下面蒋总结讨论具体用法:

1. 根据修饰对象分类

1.1 修饰代码块

synchronized(this|object) {}

synchronized(类.class) {}

1.2 修饰方法

修饰非静态方法

修饰静态方法

2. 根据获取的锁分类

2.1 获取对象锁

synchronized(this|object) {}

修饰非静态方法

2.2 获取类锁

synchronized(类.class) {}

修饰静态方法

Synchronized修饰实例方法

示例:

public class SyncDemo {
  
  private int count;
  
  public synchronized void add() {
    count++;
  }
  
  public static void main(String[] args) throws InterruptedException {
    SyncDemo syncDemo = new SyncDemo();
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        syncDemo.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        syncDemo.add();
      }
    });
    t1.start();
    t2.start();
    t1.join(); // 阻塞住线程等待线程 t1 执行完成
    t2.join(); // 阻塞住线程等待线程 t2 执行完成
    System.out.println(syncDemo.count);// 输出结果为 20000
  }
}

  

在上面的代码当中的add方法只有一个简单的count++操作,因为这个方法是使用synchronized修饰的因此每一个时刻只能有一个线程执行add方法,因此上面打印的结果是20000。如果add方法没有使用synchronized修饰的话,那么线程t1和线程t2就可以同时执行add方法,这可能会导致最终count的结果小于20000,因为count++操作不具备原子性。

Synchronized修饰静态方法

示例:

public class SyncDemo {
  
  private static int count;
  
  public static synchronized void add() {
    count++; 
  }
  
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        SyncDemo.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        SyncDemo.add();
      }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(SyncDemo.count); // 输出结果为 20000
  }
}

上面的代码最终输出的结果也是20000,但是与前一个程序不同的是。这里的add方法用static修饰的,在这种情况下真正的只能有一个线程进入到add代码块,因为用static修饰的话是所有对象公共的,因此和前面的那种情况不同,不存在两个不同的线程同一时刻执行add方法。

Sychronized修饰多个方法

示例:

public class AddMinus {
  public static int ans;
  
  public static synchronized void add() {
    ans++;
  }
  
  public static synchronized void minus() {
    ans--;
  }
  
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        AddMinus.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        AddMinus.minus();
      }
    });
  
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(AddMinus.ans); // 输出结果为 0
  }
}

在上面的代码当中我们用synchronized修饰了两个方法,addminus。这意味着在同一个时刻这两个函数只能够有一个被一个线程执行,也正是因为addminus函数在同一个时刻只能有一个函数被一个线程执行,这才会导致ans最终输出的结果等于0。

对于一个实例对象来说:

public class AddMinus {
  public int ans;
  
  public synchronized void add() {
    ans++;
  }
  
  public synchronized void minus() {
    ans--;
  }
  
  public static void main(String[] args) throws InterruptedException {
    AddMinus addMinus = new AddMinus();
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        addMinus.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        addMinus.minus();
      }
    });
  
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(addMinus.ans);
  }
}

上面的代码没有使用static关键字,因此我们需要new出一个实例对象才能够调用addminus方法,但是同样对于AddMinus的实例对象来说同一个时刻只能有一个线程在执行add或者minus方法,因此上面代码的输出同样是0。

Synchronized修饰实例方法代码块

示例:

public class CodeBlock {
  
  private int count;
  
  public void add() {
    System.out.println("进入了 add 方法");
    synchronized (this) {
      count++;
    }
  }
  
  public void minus() {
    System.out.println("进入了 minus 方法");
    synchronized (this) {
        count--;
    }
  }
  
  public static void main(String[] args) throws InterruptedException {
    CodeBlock codeBlock = new CodeBlock();
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        codeBlock.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        codeBlock.minus();
      }
    });
  
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(codeBlock.count); // 输出结果为 0
  }
}

上面的代码当中addminus方法没有使用synchronized进行修饰,因此一个时刻可以有多个线程执行这个两个方法。在上面的synchronized代码块当中我们使用了this对象作为锁对象,只有拿到这个锁对象的线程才能够进入代码块执行,而在同一个时刻只能有一个线程能够获得锁对象。也就是说add函数和minus函数用synchronized修饰的两个代码块同一个时刻只能有一个代码块的代码能够被一个线程执行,因此上面的结果同样是0。

Synchronized修饰静态代码块

示例:

public class CodeBlock {
  
  private static int count;
  
  public static void add() {
    System.out.println("进入了 add 方法");
    synchronized (CodeBlock.class) {
      count++;
    }
  }
  
  public static void minus() {
    System.out.println("进入了 minus 方法");
    synchronized (CodeBlock.class) {
        count--;
    }
  }
  
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        CodeBlock.add();
      }
    });
  
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 10000; i++) {
        CodeBlock.minus();
      }
    });
  
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(CodeBlock.count);
  }
}

上面代码的锁对象是CodeBlock.class,这个时候他不再是锁住一个对象了,而是一个类了,这个时候的并发度就变小了,当锁对象是CodeBlock.class的时候,实例对象之间时不能够并发的,因为这个时候的锁对象是一个类。

总结

Synchronized修饰实例方法:不同的对象之间是可以并发的;
Synchronized修饰静态实例方法:不同的对象是不能并发的,但是不同的类之间可以进行并发;
Sychronized修饰多个方法:多个方法在同一时刻只能有一个方法被执行,而且只能有一个线程能够执行;
Synchronized修饰实例方法代码块:同一个时刻只能有一个线程执行代码块;
Synchronized修饰静态代码块:同一个时刻只能有一个线程执行这个代码块,而且不同的对象之间不能够进行并发;

参考&致谢

深入Synchronized各种使用方法

莫笑少年江湖梦,谁不少年梦江湖.感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!