synchronized详解

synchronized摘要

在JDK1.6之前,synchronized一直以重量级锁存在,加锁解锁过程十分消耗性能。随着JDK1.6之后,JDK对synchronized锁进行了各种优化,为了减少加锁和解锁过程的消耗引入了偏向锁和轻量级锁,因此带来了锁升级过程。

synchronized锁存储结构

Java中每一个对象都可以作为锁,主要有三种形式:

(1)对于普通同步方法,锁是当前实例对象;

(2)对于静态同步方法,锁是当前类的Class对象;

(3)对于同步代码块,锁是synchronized括号内匹配的对象。

JVM底层通过进入和退出Monitor对象来实现的同步方法和同步代码块,monitorenter指令在编译后插入到同步代码块开始位置,而monitorexit插入到方法结束处和异常处。任何一个对象都有与之对应的Monitor,当一个Monitor被持有后,它处于锁定状态,线程执行到monitorenter指令时,将会尝试获取对象所对应的Monitor所有权,即尝试获取对象的锁。

例如下面一个简单的Java加锁的代码,通过javap -c Main反编译java字节码文件。

public class Main {
    private Object object = new Object();

    public void test() {
        synchronized(object) {
            System.out.println("java的架构师技术栈");
        }
    }
}

public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void test();
    Code:
       0: getstatic     #2                  // Field object:Ljava/lang/Object;
       3: dup
       4: astore_0
       5: monitorenter // 此处进入
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #4                  // String java
      11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: aload_0
      15: monitorexit // 正常退出
      16: goto          24
      19: astore_1
      20: aload_0
      21: monitorexit // 异常退出
      22: aload_1
      23: athrow
      24: return
    Exception table:
       from    to  target type
           6    16    19   any
          19    22    19   any

  static {};
    Code:
       0: new           #6                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field object:Ljava/lang/Object;
      10: return
}

Java对象头

synchronized使用的锁存储在Java对象头中,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标志位。32位的JVM的Mark Word默认存储结构如下表所示。

运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,Mark Word变化为存储以下四种数据。

锁升级

锁一共有四种状态,依次为无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着竞争逐渐升级。锁可以升级但不能降级(理论上可以降级但很麻烦)。

大多数情况下,锁不存在多线程竞争,而且很多时候是由同一线程多次获得,为了让获得锁代价更低引入了偏向锁。当一个线程访问同步块获取锁时,会在对象头和栈帧锁记录里存储偏向锁线程ID,以后该线程进入和退出同步块时不需要进行CAS加锁和解锁,只需要简单测试下对象头中是否存储着指向当前线程的偏向锁。测试成功表示线程已获得锁,测试失败则需要再测试下偏向锁标志是否设置为1(表示当前是偏向锁),若没有设置则进行CAS竞争锁,若设置了则尝试使用CAS将对象头的偏向锁指向当前线程。

当其他线程尝试竞争偏向锁时,持有偏向锁的的线程才会释放锁。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象设置成无锁状态;若线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。下面线程1为偏向锁初始化流程,线程2为偏向锁撤销流程。

线程执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,若成功当前线程获取锁,若失败其他线程竞争锁,当前线程尝试使用自旋获取锁。

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回对象头,若成功表示没有发生竞争,若失败表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争抢锁,导致锁膨胀的流程。

自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞了),一旦锁升级为重量级锁,就不会恢复到轻量级锁。当锁处理这种情况下,其他线程试图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮夺锁之争。

锁的优缺点对比

synchronized性质

可重入性:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。下面使用代码测试一下,只要a不等于3就一直递归调用方法method1。

public class Main {
    private int a = 1;
    
    public static void main(String[] args) {
        Main main = new Main();
        main.method1();
    }
    
    public synchronized void method1() {
        System.out.println("a = " + a);
        if(a == 3) {
            return;
        } else {
            a++;
            method1();
        }
    }
}

// a = 1
// a = 2
// a = 3

不可中断性:在java中表示一旦这个锁被别人抢走了,你必须等待。等别的线程释放了锁,你才可以拿到,否则就一直等下去。这一点看起来是个优点,但其实在某些场景下弊端超级大,因为假如拿到锁的线程永远的不释放,那你就要永远的等下去,造成死锁。

synchronized锁分类与使用情况

synchronized主要分为对象锁和类锁,对象锁主要锁住同步代码块和方法;类锁主要锁住static方法和class。

(1)同步代码块;

public class SynTest01 implements Runnable {
    Object object = new Object();
    
    public static void main(String[] args) {
        SynTest01 syn = new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    
    @Override
    public void run() {
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName() + "线程执行了run方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "执行2秒钟之后完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

(2)方法锁;

public class SynTest02 implements Runnable {
    public static void main(String[] args) {
        SynTest02 syn = new SynTest02();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    
    @Override
    public void run() {
        method();
    }
    
    public synchronized void method() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(3)static方法锁;

public class SynTest03 implements Runnable {
    public static void main(String[] args) {
        SynTest03 instance1 = new SynTest03();
        SynTest03 instance2 = new SynTest03();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        System.out.println("main程序运行结束");
    }
    
    @Override
    public void run() {
        method1();
    }
    
    public static synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "进入到了静态方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "离开静态方法,并释放锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(4)class锁;

public class SynTest04 implements Runnable {
    public static void main(String[] args) {
        SynTest04 instance1 = new SynTest04();
        SynTest04 instance2 = new SynTest04();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        // 线程1和线程2只要有一个还存活就一直执行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序运行结束");
    }
    
    @Override
    public void run() {
        method1();
    }
    
    public void method1() {
        synchronized (SynTest04.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "进入到了方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "离开方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


end
  • 作者:JJ(联系作者)
  • 发表时间:2021-02-15 13:27
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论