Java同步器之synchronized实现原理

一、概述

多个线程操作共享数据时,synchronized保证访问共享数据的线程安全性。synchronized是非公平锁,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,不过这种抢占的方式可以预防饥饿。

synchronized加锁方式的作用范围,区别实际是被加锁的对象的区别:

作用范围 锁对象
非静态方法 当前对象 => this
静态方法 类对象 => SynchronizedSample.class (一切皆对象,这个是类对象)
代码块 指定对象 => lock (以上面的代码为例)

先明确一点:锁是加在对象上面的,重要事情再说一遍:在对象上加锁(这也是为什么wait/notify需要在锁定对象后执行,只有先拿到锁才能释放锁)

synchronizedJava并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。

二、使用案例

SynchronizedJava中解决并发问题的一种最常用的方法,也是最简单的一种方法。

Synchronized的作用主要有三个:

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题。

从语法上讲,Synchronized总共有三种用法:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

接下来我就通过几个例子程序来说明一下这三种使用方式(为了便于比较,三段代码除了Synchronized的使用方式不同以外,其他基本保持一致)。

2.1 没有同步的情况

代码段一:

public class SynchronizedTest {
    public void method1() {
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2() {
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(() -> test.method1()).start();
        new Thread(() -> test.method2()).start();
    }
}

执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1快,所以线程2先执行完成,这个过程中线程1和线程2是同时执行的。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

2.2 对普通方法同步

代码段二:

public class SynchronizedTest {
    public synchronized void method1() {
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2() {
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(() -> test.method1()).start();
        new Thread(() -> test.method2()).start();
    }
}

执行结果如下,跟代码段一比较,可以很明显的看出,线程2需要等待线程1method1执行完成才能开始执行method2方法。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.3 静态方法(类)同步

代码段三:

public class SynchronizedTest {
    public static synchronized void method1() {
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public static synchronized void method2() {
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        final SynchronizedTest test2 = new SynchronizedTest();

        new Thread(() -> test.method1()).start();
        new Thread(() -> test2.method2()).start();
    }
}

执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使testtest2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1method2,不能并发执行。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.4 代码块同步

代码段四:

public class SynchronizedTest {
    public void method1() {
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2() {
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(() -> test.method1()).start();
        new Thread(() -> test.method2()).start();
    }
}

执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

三、原理

如果对上面的执行结果还有疑问,也先不用急,我们先来了解Synchronized的原理,再回头上面的问题就一目了然了。我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

反编译结果:

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

  public void method();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3    // String Method 1 start
       9: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}

3.1 监视器锁 - Monitor

Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象,也叫管程

Monitor是一种和Sophomore等价的同步机制。是synchronizedwait()/notify()等线程同步和线程间协作工具的基石:当我们在使用这些工具时,其实是它在背后提供了支持。

简单来说:Monitor使用锁(lock)确保了在任何情况下只有一个活跃的线程,即确保线程互斥访问临界区Monitor使用条件变量(Condition Variable)提供的等待队列(Waiting Set)实现线程间协作,当线程暂时不能获得所需资源时,进入队列等待,当线程可以获得所需资源时,从等待队列中唤醒。

关于monitorentermonitorexit这两条指令的作用,我们直接参考JVM规范中描述:

monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

通过这两段描述,我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

我们再来看一下同步方法的反编译结果:

源代码:

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

反编译结果:

public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
       0: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3    // String Hello World!
       5: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

从反编译的结果来看,方法的同步并没有通过指令monitorentermonitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

3.2 运行结果解释

有了对synchronized原理的认识,再来看上面的程序就可以迎刃而解了。

  1. 代码段2结果:虽然method1method2是不同的方法,但是这两个方法都进行了同步,并且是通过同一个对象去调用的,所以调用之前都需要先去竞争同一个对象上的(monitor),也就只能互斥的获取到锁,因此,method1method2只能顺序的执行。
  2. 代码段3结果:虽然testtest2属于不同对象,但是testtest2属于同一个类的不同实例,由于method1method2都属于静态同步方法,所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),所以也只能顺序的执行。
  3. 代码段4结果:对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。

3.3 小结

JDK 1.6之前是重量级锁,线程进入同步代码块/方法时,monitor对象就会把当前进入线程的Id进行存储,设置Mark Wordmonitor对象地址,并把阻塞的线程存储到monitor的等待线程队列中,它加锁是依赖底层操作系统的mutex相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。而JDK1.6以后引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层操作系统,就没有切换的消耗,所以,Mark Word对锁的状态记录一共有4种:无锁、偏向锁、轻量级锁和重量级锁。

偏向锁指的就是JVM会认为只有某个线程才会执行同步代码(没有竞争的环境),所以在Mark Word会直接记录线程ID,只要线程来执行代码了,会比对线程ID是否相等,相等则当前线程能直接获取得到锁,执行同步代码,如果不相等,则用CAS来尝试修改当前的线程ID,如果CAS修改成功,那还是能获取得到锁,执行同步代码。如果CAS失败了,说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。

在轻量级锁状态下,当前线程会在栈帧下创建Lock RecordLockRecord会把Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象。线程执行到同步代码时,则用CAS试图将MarkWord的指向到线程栈帧的Lock Record,假设CAS修改成功,则获取得到轻量级锁。假设修改失败,则自旋(重试),自旋一定次数后,则升级为重量级锁。

synchronized锁原来只有重量级锁,依赖操作系统的mutex指令,需要用户态和内核态切换,性能损耗十分明显。重量级锁用到monitor对象,而偏向锁则在Mark Word记录线程ID进行比对,轻量级锁则是拷贝Mark WordLock Record,用CAS+自旋的方式获取。

引入了偏向锁和轻量级锁,就是为了在不同的使用场景使用不同的锁,进而提高效率。锁只有升级,没有降级。

  1. 偏向锁:只有一个线程进入临界区;(1个线程获取锁)
  2. 轻量级锁:多个线程交替进入临界区;(多个线程使用,非同时竞争锁)
  3. 重量级锁:多线程同时进入临界区。(多个线程同时竞争锁)

四、锁升级

锁的状态总共有四种:无锁、偏向锁、轻量级锁和重量级锁。

锁的升级过程:无锁 –> 偏向锁 –> 轻量级锁 –> 重量级锁。(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

synchronized锁:根据对象头中的MarkWord根据锁标志位的不同而被复用及锁升级策略。

  • 偏向锁:MarkWord存储的是偏向的线程id
  • 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针。
  • 重量锁:MrakWord存储的是队中的monitor对象的指针。

重量级锁、轻量级锁和偏向锁之间转换

624122079.png

该图是三者的转换图,主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。

4.1 无锁

只有在调用hashCode方法时,对象头的31位,才会被修改。程序不会有锁竞争(001)。

4.2 偏向锁

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

4.2.1 偏向锁获取过程

  1. 访问MarkWord中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行步骤5;如果竞争失败,执行步骤4。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

4.2.2 偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

Java15逐步废除偏向锁。

4.3 轻量级锁

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:

JavaObject.jpg

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

4.3.1 轻量级锁的加锁过程

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示。

    轻量级锁CAS操作之前堆栈与对象的状态

    2111954866.png

  2. 拷贝对象头中的MarkWord复制到锁记录中。

  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤3,否则执行步骤4。

  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象MarkWord的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

    轻量级锁CAS操作之后堆栈与对象的状态

    1019388398.png

  5. 如果这个更新操作失败了,虚拟机首先会检查对象的MarkWord是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

4.3.2 轻量级锁的解锁过程

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

4.3.3 轻量级锁升级

  • Java6之前,默认启用,默认情况下自旋次数为10次:-XX:+PreBlockSpin=10,或者自旋线程数超过了cpu核数一半。
  • Java6之后,使用自适应自旋锁:线程如果自旋成功,下次自旋最大次数会增加,因为JVM认为既然上次成功,那么这一次很大概率也会成功,反之,如果很少自旋成功,那么下次会减少自旋次数,甚至不自旋,避免cpu空转。(根据同一个锁上一次自旋时间+拥有锁线程的状态)

4.3.4 轻量级锁和偏向锁的区别

  • 争夺轻量级锁失败时,自旋尝试抢占锁。
  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

4.4 重量级锁

synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么synchronized效率低的原因。

因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

4.5 其他优化

4.5.1 适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。

问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。

但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

4.5.2 锁粗化(Lock Coarsening)

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append() {
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

4.5.3 锁消除(Lock Elimination)

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

public class SynchronizedTest02 {
    public static void main(String[] args) {
        SynchronizedTest02 test02 = new SynchronizedTest02();
        // 启动预热
        for (int i = 0; i < 10000; i++) {
            i++;
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test02.append("abc", "def");
        }
        System.out.println("Time = " + (System.currentTimeMillis() - start));
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

虽然StringBufferappend是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。下面是我本地执行的结果:

376308694.png

为了尽量减少其他因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。通过上面程序,可以看出消除锁以后性能还是有比较大提升的。

注:可能JDK各个版本之间执行的结果不尽相同,我这里采用的JDK版本为1.6。

4.6 小结

JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

五、总结

  1. synchronized在编译时会在同步块前后生成monitorentermonitorexit字节码指令;
  2. monitorentermonitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦;
  3. monitorentermonitorexit字节码指令更底层是使用Java内存模型的lockunlock指令;
  4. synchronized是可重入锁;
  5. synchronized是非公平锁;
  6. synchronized可以同时保证原子性、可见性、有序性;
  7. synchronized锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

六、拓展

6.1 synchronized加锁this和class的区别

当使用synchronized加锁class时,无论共享一个对象还是创建多个对象,它们用的都是同一把锁,而使用synchronized加锁this时,只有同一个对象会使用同一把锁,不同对象之间的锁是不同的。

6.2 Synchronized修饰的方法抛出异常时会释放锁吗?

会,代码执行完毕或者异常结束会释放锁。

试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时。

参考文章

posted @ 2022-04-24 15:54  夏尔_717  阅读(501)  评论(0编辑  收藏  举报