Java中使用Synchronized关键字进行同步

时间:2020-01-09 10:35:06  来源:igfitidea点击:

在多线程程序中,共享资源始终是线程之间争用的焦点。如果代码中有一个关键部分,我们正在其中修改共享资源,那么我们希望对该关键部分的访问受到限制,以便在任何给定时间只有一个线程可以访问关键部分代码并使用共享资源。在Java中,实现此过程的过程称为同步,我们将在Java中使用synced关键字进行同步。

Java中的同步如何工作

Java中的每个对象都有一个与之关联的锁(也称为监视器)。当线程进入同步方法或者同步块时,它将获取该锁。尝试执行相同代码的所有其他线程(在同步方法或者同步块中)必须等待第一个线程完成并释放锁。

在这里请注意,一旦线程调用任何同步方法并获得了锁,该对象就会被锁定。这意味着在获取线程释放锁之前,无法调用该对象的所有同步方法。
因此,锁处于对象级别,并由特定对象的所有同步方法共享。

要了解如何在类级别而不是实例级别进行同步,请参阅此postSynchronization与Java中的static关键字。

在Java中使用Synchronized关键字

为了在Java中同步代码,我们可以使用以下两种方式之一:

  • 同步整个方法(同步方法)
  • 使用方法同步代码行(同步语句或者同步块)

Java中的同步方法

要使方法在Java中同步,只需在其声明中添加synced关键字即可。

Java中同步方法的一般形式

synchronized <returntype> method_name(args){
  ...
  ...
}

同步方法Java示例

让我们看一下Java中同步方法的示例,这里有两种方法:在一种方法中,有一个从1到5运行的for循环,并显示了这些值;在另一种方法中,是从5到1运行并显示了值。这里需要的是哪种方法最先运行应该显示所有值,即1,2,3,4,5和5,4,3,2,1. 首先,让我们看看如果不进行同步会发生什么。

// Class whose object will be shared
class Counter{
  public void increment(){
    for(int i = 1; i <= 5 ; i++){
      System.out.println(Thread.currentThread().getName() + " i - " + i);
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    } 
  }
  public void decrement(){
    for(int i = 5; i > 0 ; i--){
      System.out.println(Thread.currentThread().getName() + " i - " + i);		   
    } 
  }
}

public class SynchronizedDemo {
  public static void main(String[] args) throws InterruptedException {
    // One object shared among both threads
    Counter ctr = new Counter();
    Thread t1 = new Thread(){
      @Override
      public void run() {
        ctr.increment();
      }
    };		
    Thread t2 = new Thread(){
      @Override
      public void run() {
        ctr.decrement();
      }
    };
		
    t1.start();
    t2.start();
  }
}

输出:

Thread-1 i - 5
Thread-0 i - 1
Thread-1 i - 4
Thread-1 i - 3
Thread-1 i - 2
Thread-1 i - 1
Thread-0 i - 2
Thread-0 i - 3
Thread-0 i - 4
Thread-0 i – 5

如我们所见,两个线程是交织的,并且输出是混合的。

为确保显示所有值,我们可以同步方法。

// Class whose object will be shared
class Counter{
  public synchronized void increment(){
    for(int i = 1; i <= 5 ; i++){
      System.out.println(Thread.currentThread().getName() + " i - " + i);
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    } 
  }
  public synchronized void decrement(){
    for(int i = 5; i > 0 ; i--){
      System.out.println(Thread.currentThread().getName() + " i - " + i);		   
    } 
  }
}

public class SynchronizedDemo {
  public static void main(String[] args) throws InterruptedException {
    // One object shared among both threads
    Counter ctr = new Counter();
    Thread t1 = new Thread(){
      @Override
      public void run() {
        ctr.increment();
      }
    };
    
    Thread t2 = new Thread(){
      @Override
      public void run() {
        ctr.decrement();
      }
    };
    
    t1.start();
    t2.start();
  }
}

输出:

Thread-0 i - 1
Thread-0 i - 2
Thread-0 i - 3
Thread-0 i - 4
Thread-0 i - 5
Thread-1 i - 5
Thread-1 i - 4
Thread-1 i - 3
Thread-1 i - 2
Thread-1 i – 1

从输出中可以看到,一旦一个线程将对象锁定,另一个线程就无法执行该对象的任何同步方法。如果其中一个线程获取了锁并开始执行同步的incume()方法,则另一个线程将无法执行decrement()方法,因为该方法也已同步。

Java中的同步块

实现线程同步的另一种方法是使用Java中的同步块。同步语句必须指定提供内部锁的对象。

Java中同步块的一般形式

Synchronized(object_reference){
  // code block
}

在以下情况下,同步块很有用,并且可以提高性能:

  • 我们有一个大方法,但是关键部分(修改共享资源的代码)在该大方法中只有几行,那么我们只能同步该关键部分,而不同步整个方法。
  • 我们有一些对象不是为了在多线程环境中运行而设计的,并且这些方法不同步。在这种情况下,我们可以将对这些方法的调用放在同步块中。

我们可以像以前一样使用相同的示例。现在,无需同步方法,我们可以在调用方法的地方使用同步块。

// Class whose object will be shared
class Counter{
  public void increment(){
    for(int i = 1; i <= 5 ; i++){
      System.out.println(Thread.currentThread().getName() + " i - " + i);
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    } 
  }
  public void decrement(){
    for(int i = 5; i > 0 ; i--){
      System.out.println(Thread.currentThread().getName() + " i - " + i);		   
    } 
  }
}

public class SynchronizedDemo {
  public static void main(String[] args) throws InterruptedException {
    // One object shared among both threads
    Counter ctr = new Counter();
    Thread t1 = new Thread(){
      @Override
      public void run() {
        // Method call in synchronized block
        synchronized(ctr){
          ctr.increment();
        }
      }
    };
    
    Thread t2 = new Thread(){
      @Override
      public void run() {
        // Method call in synchronized block
        synchronized(ctr){
          ctr.decrement();
        }
      }
    };
    
    t1.start();
    t2.start();
  }
}

我们也可以将代码放在同步块中,而不是同步方法。

class Counter{
  public void increment(){
    // synchronized block
    synchronized(this){
      for(int i = 1; i <= 5 ; i++){
        System.out.println(Thread.currentThread().getName() + " i - " + i);
        try {
          Thread.sleep(50);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      } 
    }
  }
  public void decrement(){
    synchronized(this){
      for(int i = 5; i > 0 ; i--){
        System.out.println(Thread.currentThread().getName() + " i - " + i);		   
      } 
    }
  }
}

有关Java同步的要点

  • Java中的同步是围绕称为内部锁或者监视器锁的内部实体构建的。
  • 每个对象都有一个与之关联的固有锁。需要对对象的字段进行独占和一致访问的线程必须在访问对象之前先获取对象的固有锁,然后在完成对它们的锁定后释放固有锁。
  • 线程调用同步方法时,它将自动获取该方法对象的内在锁,并在方法返回时释放该内在锁。即使返回是由未捕获的异常引起的,也会发生锁定释放。
  • 一个线程无法获取另一个线程拥有的锁。但是线程可以获取它已经拥有的锁。允许一个线程多次获取相同的锁将启用重入同步。
  • Java中的同步会降低性能,因为线程可以顺序使用同步代码。尝试使用同步块来同步关键部分,而不是同步整个方法。
  • 在使用关键字sync的情况下,没有单独的读写锁,也没有规定允许并发读取来提高性能。如果读取次数多于写入次数,请尝试使用ReentrantReadWriteLock。
  • 不建议尝试将字符串对象用作具有同步块的锁。这是因为字符串池共享文字字符串。因此,尽管完全不相关,但多个字符串可以共享同一对象引用。这可能会导致意外行为。