(五)Java并发-Synchronized

TOC
  1. 1. 使用
    1. 1.1. 非静态同步方法
    2. 1.2. 静态同步方法
    3. 1.3. 同步代码块
  2. 2. 锁升级过程
    1. 2.1. 无锁状态
    2. 2.2. 偏向锁
    3. 2.3. 轻量级锁
      1. 2.3.0.1. 自适应自旋锁
  3. 2.4. 重量级锁
  4. 2.5. 锁消除 lock eliminate
  5. 2.6. 锁粗化 lock coarsening
  • 3. 原理
    1. 3.1. 为什么调用wait()方法需要用while()
    2. 3.2. notifyAll()唤醒
  • 使用

    当多个线程同时读写一个状态变量,如果没有同步措施,将会出现不可预见的结果。在Java中首要的同步策略是使用Synchronized关键字,它提供了可重入的独占锁。Synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,它还保证共享变量的内存可见性。
    Java中每一个对象都可以作为锁,当一个线程试图访问同步代码块时,必须先获得对象的锁。

    • Synchronized加在非静态同步方法,锁是当前实例对象
    • Synchronized加在静态同步方法,锁是当前类的class对象
    • Synchronized加在同步方法块,锁是括号里面的对象

    非静态同步方法

      非静态同步方法用的都是同一把锁——实例对象本身,当一个实例对象的非静态方法获得锁后,其他线程想要获取该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获得锁(如果其他线程访问该实例的非同步方法则不会阻塞)。注意这里是实例对象本身,如果是别的实例则不受影响。

    静态同步方法

      静态同步方法用的也是同一把锁——类对象本身,如果一个静态同步方法获得锁后,其他的静态同步方法都必须等待该方法释放锁,不管是不是同一个实例对象的静态方法还是不同实例对象的静态同步方法,只要是一个类的实例对象。静态同步方法和非静态同步方法没有竞态条件。

    同步代码块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test 
    {
    public void method1()
    {
    synchronized(this)
    {
    }
    }
    public synchronized void method2()
    {
    }
    }

    上述代码中, synchronized 代码块中传入的是 this,和同步非静态方法是一样的,当代码块获得锁后,另一个获取 method2 方法的线程必须阻塞,代码块中传入 Test.this 和 this 的作用一样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test{ 

    pubic static void method1(){
    synchronized(Test.class){
    }
    }
    public static synchronized void method2(){
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Test{ 
    public static Test test;
    public Test(){
    test=this;
    }
    public static void method1(){
    synchronized(test.getClass()){
    }
    }
    }

    上面的两段代码和静态同步方法是一样的,不能传入 this,只能使用 Class 对象。

    锁升级过程

    在JDK1.6之前,synchronized是重量级锁,加锁需要从用户态切换为内核态,会引起线程上下文切换和线程调度,增加系统开销。因此JDK1.6对Synchronized进行优化。
    image
    优化后的Synchronized分为四种状态:无锁状态->偏向锁->轻量级锁->重量级锁。

    无锁状态

    无锁状态mark word中的偏向锁位位0,锁标志位位01.

    偏向锁

    在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此引入偏向锁。偏向锁在markword上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以偏向锁就是指偏向加锁的第一个线程。如果当前对象的偏向锁标志没有设置,使用CAS竞争锁;如果设置了,尝试使用CAS将偏向锁指向当前线程。hashCode备份在线程栈上线程销毁,锁降级为无锁。

    轻量级锁

    如果存在多个线程争抢锁,锁升级为轻量级锁。每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁。线程通过CAS争用锁,会浪费CPU,因此线程争抢锁自旋10次,会升级为重量级锁,线程进入阻塞队列中,等待占用锁的线程释放锁,唤醒阻塞队列的线程(唤醒所有阻塞线程,因此Synchronized是非公平锁)。

    自适应自旋锁

    JDK1.6之后引入自适应自旋锁,意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    重量级锁

    synchronized的重量级锁基于操作系统的Mutex Lock实现,获取和释放锁会带来用户态到内核态的切换,会切换线程上下文。

    锁消除 lock eliminate

    1
    2
    3
    4
    public void add(String str1,String str2){
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
    }

    我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

    锁粗化 lock coarsening

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public String test(String str){

    int i = 0;
    StringBuffer sb = new StringBuffer():
    while(i < 100){
    sb.append(str);
    i++;
    }
    return sb.toString():
    }

    JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

    原理

    Synchronized是使用管程实现,管程对应的英文是Monitor。在管程的数据模型中,对共享变量和条件变量的操作是封装的,当线程使用共享变量时,会尝试获得锁,每次只允许一个线程获取锁进入管程,其余线程进入同步队列。管程内有条件变量的等待队列,当线程获取锁后,因不满足条件,调用wait()后,会将线程放入等待队列中。

    1
    2
    3
    while (条件不满足) {
    wait();
    }

    等待条件满足时,调用notifyAll()方法(Synchronized调用notifyAll()方法,是非公平锁),等待队列中的线程进入同步队列中,并争抢锁。

    1
    2
    3
    if (条件满足) {
    notifyAll();
    }

    image

    为什么调用wait()方法需要用while()

    因为调用wait()方法进入等待队列的线程,不需要经过notify()也可以退出等待状态,比如线程结束后自动退出等待状态。另外也可能调用notify()之后,条件又不满足了,还需要再进入等待队列中等待资源。

    notifyAll()唤醒

    • notify来唤起的线程,先进入wait的线程会先被唤醒
    • notifyAll唤起的线程,默认情况使用LIFO策略,最后进入的会先被唤醒
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      public class NotifyTest {
      private static final Object lock = new Object();
      public static void main(String[] args) {
      for (int i = 0; i < 5; i++) {
      new Thread(()-> {
      synchronized (lock) {
      System.out.println(Thread.currentThread().getName()+ "_准备调用wait方法!");
      try {
      lock.wait();
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName()+ "_收到notify通知,执行后续操作!");
      }
      }, "thread_"+i).start();
      }
      new Thread(()-> {
      synchronized (lock) {
      System.out.println(Thread.currentThread().getName()+ "_等待7秒后调用notify通知所有等待线程!");
      try {
      Thread.sleep(7 *1000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      lock.notifyAll();
      }
      }, "thread_notify").start();
      for (int i = 1000; i < 1005; i++) {
      new Thread(()-> {
      System.out.println(Thread.currentThread().getName() + "_进入同步队列,准备争抢锁!");
      synchronized (lock) {
      System.out.println(Thread.currentThread().getName()+ "_同步队列获取锁资源了!");
      }
      }, "thread_"+i).start();
      }
      }
      }
      thread_0_准备调用wait方法!
      thread_3_准备调用wait方法!
      thread_2_准备调用wait方法!
      thread_1_准备调用wait方法!
      thread_4_准备调用wait方法!
      thread_notify_等待7秒后调用notify通知所有等待线程!
      thread_1000_进入同步队列,准备争抢锁!
      thread_1001_进入同步队列,准备争抢锁!
      thread_1002_进入同步队列,准备争抢锁!
      thread_1004_进入同步队列,准备争抢锁!
      thread_1003_进入同步队列,准备争抢锁!
      thread_4_收到notify通知,执行后续操作!
      thread_1_收到notify通知,执行后续操作!
      thread_2_收到notify通知,执行后续操作!
      thread_3_收到notify通知,执行后续操作!
      thread_0_收到notify通知,执行后续操作!
      thread_1003_同步队列获取锁资源了!
      thread_1004_同步队列获取锁资源了!
      thread_1002_同步队列获取锁资源了!
      thread_1000_同步队列获取锁资源了!
      thread_1001_同步队列获取锁资源了!