多线程同步控制关键词 synchronized 同步锁介绍

2020-11-03 0 By admin

在程序的设计语言中,【锁提供了一种数据安全访问的方式,锁一般分为加锁和解锁两个操作。对共享数据操作前,要先进行加锁;操作完成后,再进行解锁。】
加锁以后的【临界区只能被持有锁的线程占有,其他线程不能进入这段临界区,只能等待】。Java 语言提供同步锁、可重入锁和读写锁等同步机制,用于确保数据访问的正确性。

一、同步锁 synchronized

在多线程开发过程中,我们通常会用到 synchronized 关键字,一般称之为“同步锁”;用它来修饰需要同步的方法和需要同步代码块。

  • 默认情况下,是使用当前对象作为锁的对象。
  • 在用 synchronized 修饰类时(或者修饰静态方法),使用当前类的 Class对象作为锁的对象。
  • 故存在着方法锁、对象锁、类锁这样的概念。

在 JAVA 语言中,从 JDK1.0 开始就支持同步锁的使用了。它可以采用两形式:同步方法和同步代码块。不论哪种方式,都需要使用 synchronized 关键字,但两者表现形式不同。

1.1、同步方法

采用 synchronized 作为方法的修饰词,将方法整体限定在同步控制区域内;同一时刻只能有一个线程对其进行访问。

1.2、同步代码块

同步代码块是使用 synchronized 修饰的一块代码,它不像同步方法那样使整个方法都是被同步控制,而是针对某一块代码进行同步控制。
同步代码块需要明确地指出监视器对象,通常加载 synchronized 后的小括内,使用比较多的情况是使用当前对象 this 作为监视器对象。

1.3、两种方式比较

同步代码块比同步方法可以实现更细粒度的同步控制,但同步方法的使用更加简便,不用考虑同步对象等因素。
但是,有时整个方法加上 synchronized 块,程序性能并不好,这是因为函数内部可能需要同步的只有小部分共享数据而已。
需要注意的是,这两种方法都是使用 JVM 内置的监视器。

二、示例:没有设置线程同步的情况

场景:学生军训,有三列,每列5人,需要报数,每个线程负责每一列报数。

class SynchronizedExample {
  protected static int num = 0;
  protected void numberOff() {
    for(int i=0; i<5; i++) {
      num++;
System.out.println(Thread.currentThread().getName()+":"+SynchronizedExample.num);
    }
  }
}

public class SynchronizedTest {
  public static void main(String[] args) throws InterruptedException {
    SynchronizedExample se = new SynchronizedExample();
    for(int i=1; i<=3; i++) {
      new Thread( ()->  {se.numberOff();}, "线程"+i).start();
    }
  }
}

执行结果如下:
线程1:1
线程2:2
线程1:3
线程3:4
.......

之所以出现这种情况,是因为三个线程是异步的,没有同步。
对应的业务场景就是,在第一列没有完成报数的时候,其他队列抢报了,这在现实中是不允许的。

可以通过 synchronized 等具有同步功能的关键字设置处理。

二、方法同步锁

当报数方法加上 synchronized关键字 之后,就会一列一列的报数。

protected synchronized void numberOff() {
  for(int i=0; i<5; i++) {
    num++;
System.out.println(Thread.currentThread().getName()+":"+SynchronizedExample.num);
  }
}

执行结果如下:
线程1:1
线程1:2
线程1:3
线程1:4
......

当一个线程执行带有 synchronized 关键字的方法时,该线程会在该方法处设置一个锁(其他线程打不开这个锁,只能在外边等该线程释放掉该锁,一般都是执行完所有代码后主动释放锁),表示此方法是当前线程独占的,对应到上述业务中就是一次只能有一个队列报数。

补充:这种写法,【不能保证线程拿到锁的顺序】;也就是不能保证第一列首先报数,然后是第二列、第三列。多线程编程本身就是不能确定线程的执行顺序的,总之还是这个示例场景选的不合适。

三、对象锁

改进后的代码用到了一个对象锁,该对象锁默认是当前对象,上述代码等同于以下代码:

protected void numberOff() {
  synchronized (this) {
    for (int i = 0; i < 5; i++) {
      num++;
      System.out.println(Thread.currentThread().getName() + ":" + SynchronizedExample.num);
    }
  }
}

当多个线程用一个对象锁,各个线程可以达到同步的作用,如果每个线程都用自己的对象锁,那么 synchronized 就失去了同步的作用。

class SynchronizedExample {
  protected static int num = 0;
  protected void numberOff() {
    synchronized (this) {
      for (int i = 0; i < 5; i++) {
        num++;
      System.out.println(Thread.currentThread().getName() + ":" + SynchronizedExample.num);
      }
    }
  }
}
public class SynchronizedTest {
  public static void main(String[] args) throws InterruptedException {
    for(int i=1; i<=3; i++) {
      new Thread( ()->  {new SynchronizedExample().numberOff();}, "队列"+i).start();
    }
  }
}

执行结果如下:
线程1:1
线程2:2
线程1:3
线程3:4
.......

四、类锁

对于上述问题,读者应该得出一个结论,要想达到同步的效果,必须用同一个锁,此时类锁可以粉末登场。

protected void numberOff(Object lock) {
  synchronized (SynchronizedExample.class) { //使用类
    for (int i = 0; i < 5; i++) {
      num++;
      System.out.println(Thread.currentThread().getName() + ":" + SynchronizedExample.num);
    }
  }
}

上述代码可以达到同步的效果。

五、静态锁

静态锁是针对静态方法而言,当一个静态方法中有 synchronized 关键字时,默认的是使用当前类字节码对象作为锁。

class SynchronizedExample {
  protected static int num = 0;
  protected synchronized static void numberOff() {
    for (int i = 0; i < 5; i++) {
      num++;
      System.out.println(Thread.currentThread().getName() + ":" + SynchronizedExample.num);
    }
  }
}

public class SynchronizedTest {
  public static void main(String[] args) throws InterruptedException {
    for (int i = 1; i <= 3; i++) {
      new Thread(() -> { new SynchronizedExample().numberOff(); }, "队列" + i).start();
    }
  }
}

六、线程池实现

最后用线程池将上述代码写一下

package ioo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class SynchronizedExample {
  protected static int num = 0;
  protected synchronized static void numberOff() {
    for (int i = 0; i < 5; i++) {
      num++;
      System.out.println(Thread.currentThread().getName() + ":" + SynchronizedExample.num);
    }
  }
}

public class SynchronizedTest {
  public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for(int i=1; i<=3; i++) {
      executorService.execute(() -> new SynchronizedExample().numberOff());
    }
  }
}