synchronized原理详解

小鱼儿 2023-07-20 10:39 96阅读 0赞

文章目录

    • synchronized关键字
    • 保证并发三大特性
    • synchronized的两个特性
        • 可重入特性
        • 不可中断特性
    • Java对象
    • synchronized原理
        • monitor监视器锁
        • monitor竞争
        • monitor等待
        • monitor释放
        • synchronized是重量级锁
    • CAS
        • CAS原理
        • CAS适用场景
    • synchronized锁升级过程
        • 偏向锁
        • 轻量级锁
        • 自旋
        • 重量级锁
        • 锁消除
        • 锁粗化
    • 示例
    • synchronized小结
    • synchronized和Lock的区别
    • 平时写代码如何对synchronized优化

synchronized关键字

synchronized可以把任意一个非NULL的对象当做锁,HotSpotVM中,这个锁称为 对象监视器(Object Monitor):

  • 作用于静态方法时,锁住的是Class实例,相当于一个全局锁,对所有调用这个静态方法的线程有效。
  • 作用于非静态方法时,锁住的是对象的实例(this)。
  • 作用于一个代码块时,可以通过自定义任意一个对象obj当做锁,锁住的是所有以该对象为锁的代码块。

保证并发三大特性

synchronized能保证可见性、原子性和有序性

synchronized保证可见性原理
执行到synchronized代码块时,JVM会执行lock这个原子操作,将这个线程的工作内存中共享变量的副本值清空,之后这个线程再次需要用这些变量的时候,发现自己工作内存中的值已经失效了,就会重新从主存中获取最新值。当执行完同步代码块时,JVM会执行unlock这个原子操作,工作内存的变量副本会立刻同步到主存。

synchronized保证原子性原理
只有获取到锁的线程才能去执行synchronized中的代码块,即使中途发生线程切换,这个线程持有的锁不会释放,所以这期间其他线程也无法获取到锁去执行这个代码块。但是发生异常的话,Synchronized锁会自动释放。

synchronized保证有序性原理
加上了synchronized关键词的代码块,编译器还是会进行代码重排序的优化,只是synchronized保证了代码块只能同步访问,下一个线程获取锁之后,上一个线程对共享变量做的改变对它是可见的,就是通过lock刷新工作内存的机制,这样,编译器优化的问题就不会出现。

synchronized的两个特性

可重入特性

指的是同一线程的外层函数获得锁之后,内层函数还可以再次获取该锁,也就是说一个线程可以多次获取同一把锁。
好处

  • 避免死锁(对于不可重入锁,如果A方法中调用B方法,A和B方法都要获取同一把锁,A方法先获取了锁,去调用B方法的时候,B方法此时要等A释放锁才能执行,但是A方法要等B方法执行完了才能释放锁,这就造成了死锁)
  • 可以让我们更好的来封装代码,而不是将所有的逻辑都写在一个同步方法中

示例

  1. public class Test {
  2. public static void main(String[] args) {
  3. new MyThread().start();
  4. new MyThread().start();
  5. }
  6. }
  7. // 自定义一个线程类
  8. class MyThread extends Thread {
  9. @Override
  10. public void run() {
  11. synchronized (MyThread.class) {
  12. System.out.println(getName() + "进入了同步代码块1");
  13. synchronized (MyThread.class) {
  14. System.out.println(getName() + "进入了同步代码块2");
  15. }
  16. // 这个代码块可以是调用本类或者其他类方法中的同步代码块
  17. // test0();
  18. }
  19. }
  20. // public void test0() {
  21. // synchronized (MyThread.class) {
  22. // System.out.println(getName() + "进入了同步代码块2");
  23. // }
  24. // }
  25. }

结果:

  1. Thread-0进入了同步代码块1
  2. Thread-0进入了同步代码块2
  3. Thread-1进入了同步代码块1
  4. Thread-1进入了同步代码块2

实现原理:
同一个线程能多次获取同一把锁进入synchronized代码块中执行,实现原理是利用了一个变量记录(假设微lockedThread)当前获取到锁的线程,还有一个变量(假设微lockedCount)记录当前锁被获取的次数,当一个线程尝试获取锁时,如果这个锁已经被获取过了,会去判断获取到锁的线程是不是就是当前这个线程,如果是,则对lockedCount变量进行加1,获取锁成功。释放锁的时候,先对lockedCount变量进行减1,只有当lockedCount减为0的时候,才会真正释放锁。下面给出一个简易模拟代码:

  1. public class Lock {
  2. boolean isLocked = false; // 标识锁是否被线程获得
  3. Thread lockedBy = null; // 记录获得锁的线程
  4. int lockedCount = 0; // 记录一个线程中,锁被获取的次数
  5. public synchronized void lock() throws InterruptedException {
  6. Thread thread = Thread.currentThread(); // 当前尝试获取锁的线程
  7. // 锁已经被线程获得,并且获取锁的线程不是当前线程,则当前线程等待
  8. while(isLocked && lockedBy != thread) {
  9. wait();
  10. }
  11. isLocked = true;
  12. lockedBy = thread;
  13. lockedCount++;
  14. }
  15. public synchronized void unlock() {
  16. lockedCount--;
  17. // 该线程中,获取锁的程序都执行了释放锁操作,线程才真正释放锁
  18. if(0 == lockedCount) {
  19. isLocked = false;
  20. notify();
  21. }
  22. }
  23. }

不可中断特性

一个线程获得锁后,其他尝试获取这个锁的线程只能等待或者阻塞,不能被中断,只能一直等着拥有锁的线程释放锁。

  1. public class Test {
  2. private static Object obj = new Object();
  3. public static void main(String[] args) throws InterruptedException{
  4. Runnable runnable = () -> {
  5. synchronized (obj) {
  6. String name = Thread.currentThread().getName();
  7. System.out.println(name + "进入同步代码块");
  8. // 保证不退出同步代码块
  9. try {
  10. Thread.sleep(88888);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }finally {
  14. System.out.println(name + "执行结束");
  15. }
  16. }
  17. };
  18. Thread t1 = new Thread(runnable);
  19. t1.start();
  20. Thread.sleep(1000);
  21. Thread t2 = new Thread(runnable);
  22. t2.start();
  23. System.out.println("停止第二个线程之前");
  24. t2.interrupt();
  25. System.out.println("停止第二个线程之后");
  26. System.out.println(t1.getState());
  27. System.out.println(t2.getState());
  28. }
  29. }
  30. Thread-0进入同步代码块
  31. 停止第二个线程之前
  32. 停止第二个线程之后
  33. TIMED_WAITING #第一个线程进入等待状态
  34. BLOCKED #第二个线程进入阻塞状态
  35. Thread-0执行结束
  36. Thread-1进入同步代码块
  37. Thread-1执行结束
  38. java.lang.InterruptedException: sleep interrupted
  39. at java.lang.Thread.sleep(Native Method)
  40. at com.exapmle.service.test9.Test.lambda$main$0(Test.java:14)
  41. at java.lang.Thread.run(Thread.java:748)

Lock锁可以设置为可中断的,也可以设置为不可中断的

  1. public class LockTest {
  2. private static Lock lock = new ReentrantLock();
  3. public static void main(String[] args) throws InterruptedException {
  4. test02();
  5. }
  6. // 不可中断锁 lock()
  7. public static void test01() throws InterruptedException {
  8. Runnable runnable = () -> {
  9. String name = Thread.currentThread().getName();
  10. try{
  11. lock.lock();
  12. System.out.println(name+"获取锁");
  13. // 让获取锁的线程暂时不退出
  14. Thread.sleep(88888);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. } finally {
  18. lock.unlock();
  19. System.out.println(name+"释放锁");
  20. }
  21. };
  22. Thread t1 = new Thread(runnable);
  23. t1.start();
  24. Thread.sleep(1000);
  25. Thread t2 = new Thread(runnable);
  26. t2.start();
  27. System.out.println("停止第二个线程之前");
  28. t2.interrupt();
  29. System.out.println("停止第二个线程之后");
  30. Thread.sleep(1000);
  31. System.out.println(t1.getState());
  32. System.out.println(t2.getState());
  33. }
  34. // 可中断锁 tryLock()
  35. public static void test02() throws InterruptedException {
  36. Runnable runnable = () -> {
  37. String name = Thread.currentThread().getName();
  38. boolean b = false;
  39. try{
  40. // 3s之内没有获取到锁,就停止阻塞,去执行其他任务
  41. b = lock.tryLock(3, TimeUnit.SECONDS);
  42. if(b) {
  43. System.out.println(name+"获取锁");
  44. // 让获取锁的线程暂时不退出
  45. Thread.sleep(88888);
  46. }else{
  47. System.out.println(name+"没有获取到锁");
  48. }
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. } finally {
  52. if(b) {
  53. lock.unlock();
  54. System.out.println("name"+"释放锁");
  55. }
  56. }
  57. };
  58. Thread t1 = new Thread(runnable);
  59. t1.start();
  60. Thread.sleep(1000);
  61. Thread t2 = new Thread(runnable);
  62. t2.start();
  63. }
  64. }

test01执行结果:

  1. Thread-0获取锁
  2. 停止第二个线程之前
  3. 停止第二个线程之后
  4. TIMED_WAITING
  5. WAITING # 虽然执行了t2.interrupt(); #但是线程2并没有被中断,而是进入等待状态
  6. Thread-0释放锁
  7. Thread-1获取锁 # 等线程1释放锁之后,第二个线程获取锁
  8. Thread-1释放锁
  9. java.lang.InterruptedException: sleep interrupted # 中断异常,因为不可中断
  10. at java.lang.Thread.sleep(Native Method)
  11. at com.exapmle.service.test9.LockTest.lambda$test01$0(LockTest.java:22)
  12. at java.lang.Thread.run(Thread.java:748)
  13. Process finished with exit code 0

test02执行结果:

  1. Thread-0获取锁
  2. Thread-1没有获取到锁 #3s之内没有获取到锁,就停止阻塞,去执行其他任务
  3. name释放锁

Java对象

在JVM中,对象在内存中的布局分为三块区域:

  • 对象头:Java对象头在32位虚拟机上占64bit、在64位虚拟机上占96bit
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

在这里插入图片描述

对象头

Hotspot虚拟机的对象头包括两部分,Mark Word(标记字段,64位虚拟机上占64bit)、Klass Pointer(类型指针,Hotspot虚拟机默认开启指针压缩,所以64位虚拟机上占32bit。没有开启指针压缩时一个指针占8字节。开启指针压缩是因为对象太大,会减小缓存命中率,GC开销增大。使用参数-XX:-UseCompressedOops可以关闭指针压缩):

  • Klass Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个Class
  • Mark Word用于存储对象自身的运行时数据,如:HashCode、GC分代年龄、Synchronized锁状态标志
    是实现Synchronized锁的关键,Synchronized加锁实际是对对象头状态的改变

Synchronized锁状态:无锁不可偏向、无锁可偏向(但还没偏向)、偏向锁、轻量级锁、重量级锁

Mark Word:
在这里插入图片描述
Lock Record
Lock Record是线程私有的数据结构,每一个被锁住的对象Mark Word都会和一个Lock Record关联,用于存储锁对象的Mark Word的拷贝。

synchronized原理

  1. public class Test {
  2. private static Object obj = new Object();
  3. // synchronized作用在代码块上
  4. public static void main(String[] args) {
  5. synchronized (obj) {
  6. System.out.println("1");
  7. }
  8. }
  9. // synchronized作用在方法上
  10. public synchronized void fun() {
  11. System.out.println("2");
  12. }
  13. }

现在通过反汇编字节码文件看看synchronized汇编源码:

  1. javap -p -v 字节码文件xx.class #-p是显示所有方法 -v是显示所有细节

在这里插入图片描述
synchronized是通过monitor监视器锁来实现,monitor对象中有两个主要属性:owner记录当前拥有锁的线程,recursion记录当前锁被获取的次数。对于synchronized修饰的代码块,在源码编译成字节码的时候,会在同步代码块的入口和出口分别插入monitorenter和monitorexit这两个字节码指令。对于synchronized修饰的方法,会在该方法上添加ACC_SYNCHRONIZED的标识,表示它是一个同步方法。

monitorenter字节码指令
当程序执行到monitorenter指令时会尝试去获取当前锁对象对应的monitor权限:

  • 如果monitor的recursion为0,则该线程进入monitor,然后将recursion设置为1、owner设置为当前线程,该线程成为monitor的所有者;
  • 如果线程已经占用了该monitor(即判断到recursion不为0,然后看owner是否为当前线程),说明这时候是持有锁的线程再次获取这个锁,则可以获取成功,将recursion加1
  • 如果其他线程占用了monitor,则该线程通过自旋操作再次尝试几次去获取锁,如果还没有获取到就被阻塞

monitorexit字节码指令
当执行到monitorexit指令时会将recursion的值减1,当这个值减到0的时候,当前线程就不再拥有这个monitor对象的所有权,就会释放锁,然后其他被这个锁阻塞的线程就可以尝试去获取这个monitor对象。

从上面反汇编结果图中看到还存在一个monitorexit指令,下面有一个Exception table,记录的是有可能出现异常的指令。这就说明,同步代码块中如果出现了异常,那么会执行到monitorexit指令将recursion减1。所以,synchronized出现异常时会释放锁。

在这里插入图片描述
当synchronized作用于方法上时,会给这个方法设置一个叫做ACC_SYNCHRONIZED的标识。当一个方法被调用时,会检测方法是否设置了ACC_SYNCHRONIZED标识,如果设置了,线程会去获取monitor,获取成功之后才能执行同步方法体,执行完后释放monitor所有权。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

monitor监视器锁

monitor监视器锁也就是通常说的synchronized对象锁。任何一个Java对象都有一个Monitor与之关联,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的是Monitor对象的指针),这就是为什么Java中任意对象可以作为锁的原因。
一个Monitor被持有后,它将处于锁定状态。Monitor是由ObjectMonitor实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的。

  1. //ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,
  2. //用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),
  3. //_owner指向持有ObjectMonitor对象的线程
  4. ObjectMonitor() {
  5. _header = NULL;
  6. _count = 0; // 重入次数
  7. _waiters = 0, // 等待线程数
  8. _recursions = 0;
  9. _object = NULL;
  10. _owner = NULL; // 当前持有锁的线程
  11. _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
  12. _WaitSetLock = 0 ;
  13. _Responsible = NULL ;
  14. _succ = NULL ;
  15. _cxq = NULL ;
  16. FreeNext = NULL ;
  17. _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表,有资格成为候选资源的线程
  18. _SpinFreq = 0 ;
  19. _SpinClock = 0 ;
  20. OwnerIsThread = 0 ;
  21. }

monitor竞争

当有多个线程竞争锁时,流程如下:
在这里插入图片描述

monitor等待

当一个线程尝试获取锁,如果锁已经被其他线程获取到了,它会再次自旋尝试获取锁,如果还是获取不到,则进行下面的流程:
在这里插入图片描述
第2步要通过CAS把node节点push到_cxq列表中,因为一次push操作可能失败

monitor释放

  1. 执行完同步代码快时会让_recursions减1,当_recursions减为0时,释放该锁
  2. 根据不同的策略唤醒等待该锁的其他线程

synchronized是重量级锁

Synchronized实现线程互斥会导致用户态和内核态的切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。所以说synchronized是重量级锁,JDK6开始对Synchronized进行了锁升级优化

CAS

CAS(Compare And Swap)比较相同再交换,是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。

在concurrent并发包下提供的AtomicInteger类就使用了CAS保证并发操作下对共享变量自增操作的正确性:

  1. import java.util.concurrent.atomic.AtomicInteger;
  2. public class Test {
  3. private static AtomicInteger atomicInteger = new AtomicInteger();
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread[] threads = new Thread[10];
  6. for(int i = 0; i < 10; i++) {
  7. threads[i] = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. for(int j = 0; j < 1000; j++) {
  11. // 自增操作
  12. atomicInteger.incrementAndGet();
  13. }
  14. }
  15. });
  16. threads[i].start();
  17. }
  18. // join()的意思是等待所有的子线程都执行完了,主线程才继续往后执行
  19. for(Thread t : threads) {
  20. t.join();
  21. }
  22. System.out.println(atomicInteger.get());//10000
  23. }
  24. }

CAS原理

CAS操作依赖3个值:内存中的值V,旧的估计值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中
AtomicInteger和Unsafe部分源码:

  1. package java.util.concurrent.atomic;
  2. public class AtomicInteger extends Number implements java.io.Serializable {
  3. // ...
  4. private static final long valueOffset; // 根据AtomicInteger对象的内存地址和偏移量valueOffset就能找到value的内存地址
  5. private volatile int value; // 保存实际的值,用volatile修饰,保证可见性
  6. /** * Atomically increments by one the current value. * * @return the updated value */
  7. public final int incrementAndGet() {
  8. // 调用了sun.misc.getAndAddInt()
  9. return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  10. }
  11. // ...
  12. }
  13. package sun.misc;
  14. public final class Unsafe {
  15. // ...
  16. public final int getAndAddInt(Object var1, long var2, int var4) {
  17. int var5; // 旧的预估值,就是此时内存中的值
  18. do {
  19. // var1:AtomicInteger对象
  20. // var2:AtomicInteger对象中的偏移量valueOffset
  21. var5 = this.getIntVolatile(var1, var2);
  22. //CAS操作,比较相同则交换一个int值
  23. // var5+var4:要修改的值
  24. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  25. return var5;
  26. }
  27. // ...
  28. }
  • 现在线程1和线程2都在对atomicInteger.value执行自增操作,假设线程2线先执行
  • 假设现在atomicInteger.value是0,线程2执行到getAndAddInt()时,取到旧的预估值就是0
  • 此时CPU切换执行线程1,线程1执行到getAndAddInt()时,取到旧的预估值也是0
  • 线程1执行`compareAndSwapInt(var1, var2, var5, var5 + var4),var1和var2结合找到当前内存中的最新值【现在是0】,var5就是旧的预估值(之前取的内存中的值)【现在是0】,var4就是自增操作加1的这个【数值1】,现在比较内存最新值【0】和预估值【0】相等,则将var5+var4的值【1】赋给内存中的这个值。
  • 线程1赋值成功,compareAndSwapInt()返回true,线程1结束
  • 此时切换回线程2,找到当前内存最新值【1】,线程1旧的预估值还是【0】,比较两者不相等,compareAndSwapInt()返回false,继续执行do while循环,此时重新取内存中的值【1】给var5,然后再执行compareAndSwapInt()。此时找到当前内存最新值【1】,就的预估值【2】,两者相等,则将var5+var4【2】赋给内存中的这个值
  • 线程1赋值成功,compareAndSwapInt()返回true,线程1结束
  • 最终,内存中的值是2
  • 乐观锁:认为读多写少,遇到并发写的可能性很小,因此每次去拿数据的时候都认为别的线程不会修改,就不会上锁,但是在更新数据的时候会判断当前数据是否被修改过了。
    Java中的乐观锁基本都是通过CAS实现的
  • 悲观锁:认为写的情况很多,遇到并发写的可能性高,因此每次去拿数据的时候都会认为别的线程会修改这个数据,因此线程一上来执行就加锁,直到执行完才释放锁
    synchronized和ReentrantLock属于悲观锁

CAS适用场景

  • CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于多核CPU下线程间竞争不激烈的场景
  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,那线程在getAndAddInt()中的do while循环处肯定会发生多次重试,反而会影响效率

synchronized锁升级过程

HotSpot虚拟机中,JDK1.6之前,synchronized是重量级锁,即使是线程交替执行无竞争并发的情况下,一个线程也要执行Synchronized加锁,进行用户态和内核态的切换。

JDK6开始对锁进行了改进和优化,使得线程之间更高效地操作共享数据,以及解决竞争问题,从而提高程序运行效率。在JDK6中,synchronized锁粒度是一个升级的过程:无锁->偏向锁->轻量级锁-> 重量级锁

锁存在四种状态依次是:无锁状态(可偏向和不可偏向)、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

偏向锁

HotSpot研究者发现,大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获得与释放,为了让线程获得锁的代价更低,引入了偏向锁。偏向锁就是这个锁会偏向第一个获得锁的线程,在整个运行过程中只有一个线程的时候是这种锁。

无锁—>偏向锁

  • A线程访问同步代码块,使用CAS操作将Thread ID放到锁对象的Mark Word中
  • 如果CAS操作成功,此时线程A获取到锁
  • 如果CAS操作失败,证明还有别的线程持有锁,则启动偏向锁撤销

偏向锁—>撤销

  • 让A线程在全局安全点阻塞
  • 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态
  • 恢复A线程,将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程

优缺点

  • 在单线程重复执行同步代码块时提升了性能,因为如果只有一个线程执行同步代码块,就没必要调用操作系统内核加锁。
  • 如果有很多线程竞争锁,偏向锁是无效的,还因为撤销偏向锁的动作必须等待全局安全点才行,反而降低了性能。

适用场景
适用于单线程反复进入同步代码块的情况。

JVM开启/关闭偏向锁

  • 开启偏向锁(JDK1.6之前):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 开启偏向锁(JDK1.6及其之后):-XX:BiasedLockingStartupDelay=0
    JDK6之后默认开启,JDK1.8中默认会延迟4s后才开启偏向锁,这是为了提高JVM启动速度,使用参数-XX:BiasedLockingStartupDelay=0可以关闭延迟。听说在JDK11中没有延迟,可以自己验证下哦!
  • 关闭偏向锁:-XX:-UseBiasedLocking

参数-XX:+PrintFlagsInitial打印出的信息中显示了偏向锁默认延迟时间(JDK1.8):
在这里插入图片描述

偏向锁提升单线程反复执行同步代码快性能的原理之一
按照HotSpot的设计,每次加锁/解锁都会涉及一些CAS操作,CAS操作会延迟本地调用。偏向锁的做法是一旦线程获得了这个锁,这个线程之后再次执行执行获取这个锁是不用走加锁/解锁操作的,即只需要判断当前是偏向锁并且锁的拥有者是它自己就行。
CAS为什么会延迟本地调用?
多核cpu、并发情况下,用volatile修饰的共享变量要保证可见性,假如此时core1和core2同时把一个共享变量拷贝到了自己的cpu缓存中,当core1修改了这个共享变量的值,通过总线写回到主存的时候,通过总线嗅探机制,会使core2中对应的这个值失效,也就是将他的缓存清空,当core使用这个变量发现数据失效了,就会重新取主存中的这个变量,这种通过总线监听来回通信称为“Cache 一致性流量”。core1和core2的值再一次相等时,称为“Cache一致性”。
而CAS刚好导致了Cache一致性流量情况加重,偏向锁通过消除不必要的CAS降低了Cache一致性流。

轻量级锁

在多线程交替执行同步代码快的情况下(就是线程A执行完了线程B才来执行,线程B执行完了线程C才来执行…,多个线程之间不会有锁竞争的情况),会使用轻量级锁。如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁。

轻量级锁加锁过程

  • 在A线程栈帧中建立一个锁记录(Lock Record),将Mark Word拷贝到自己栈帧中的Lock Record中,这个位置叫 displayced hdr
    在这里插入图片描述
  • 将Lock Record中的owner指向锁对象
  • 使用CAS操作将Lock Record的地址记录到Mark Word中,如果操作成功则进行下一步,否则进行最后一步 在这里插入图片描述
  • CAS操作成功,那么这个线程就获取到了这个锁,然后将锁标志位设为轻量级锁模式(00)
  • CAS操作失败,JVM首先会检查锁对象Mark Word中是否已经指向了当前栈帧,如果是则说明是锁重入;否则说明多个线程竞争锁,轻量级锁升级为重量级锁,不过在这之前还有自旋操作

这里为什么要使用CAS操作?
假如A、B两个线程都将MarkWord拷贝到自己栈帧中的LockRecord中,A线程先将MarkWord更新为指向自己LockRecord的指针,A线程就算获取锁成功了;B线程在执行CAS操作将MarkWord更新为指向自己LockRecord的指针,发现MarkWord变了,CAS操作就会失败,说明存在锁竞争,则锁开始膨胀。

轻量级锁释放过程

  • 取出Lock Record中保存的Mark Word信息,用CAS操作将取出的数据重新赋值到Mark Word中,操作成功,则释放锁成功
  • 否则,说明其他线程尝试获取锁,需要升级为重量级锁

轻量级锁加锁过程中为什么要把对象头里的Mark Word复制到线程栈的锁记录中?
因为升级为轻量级锁是在多线程的情况下,这些线程可能会竞争锁,那么获取到锁的线程将自己栈帧中的LockRecord地址记录到MarkWord中时要进行CAS操作,如果发现MarkWord中的值发生了变化,那CAS操作失败,说明存在锁竞争。

优点
在多线程交替执行同步代码快的情况下,可以避免重量级锁引起的性能消耗。

自旋

重量级锁的开销很大,要尽量避免轻量级锁转为重量级锁。因此,当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数依然没有拿到,锁就会升级为重量级锁。自旋锁是JDK4中引入的,在JDK6中才默认开启。

JVM开发团队发现在很多应用中,共享数据的锁定状态只会持续很短的一段时间,如果为了这么短的一段时间使线程阻塞和唤醒导致的开销不值得。先进行自旋,这个线程就不会放弃处理器执行时间而挂起。
自旋次数默认是10次,可以使用参数-XX:PreBlockSpin来更改。这个自旋次数不好确定,在JDK6中引入了适应性自旋锁。

适应性自旋锁:
自适应意味着自旋次数不再固定,而是由前一个尝试获取这个锁的线程自旋时间来决定。假如前一个线程自旋10次就获得了锁,JVM会认为这个锁很容易获取,那么当前这个线程也可以自旋10次或者再多几次就能获取到锁。假如前面的线程自旋了很多次还没有获取到锁,JVM会认为这个锁很难获取,那以后要获取这个锁时就不再进行自旋过程,以免浪费资源。

monitor锁的竞争过程就用到了自适应自旋锁。

适用场景
线程持有锁的时间短,否则自旋时间长对CPU也会造成压力。

重量级锁

synchronized锁是通过对象关联的一个叫做监视器锁(monitor)的对象来实现的,监视器锁本质又是依赖于底层的操作系统Mutex Lock来实现,而操作系统实现线程之间的切换就需要用户态和内核态的转换,这个成本很高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

锁消除

锁消除是JDK6对锁的优化。下面这段代码,StringBuffer是线程安全的,append方法上加了synchronized关键字,但是对于new StringBuffer().append(s1).append(s2).append(s3).toString()这行代码,锁对象是this,也就是StringBuffer的一个实例,每个线程执行到这句代码时,都会实例化一个StringBuffer对象,它们的锁和要锁住的资源是不同的,因此也就没必要在append方法上加锁,因此JVM会自动将这个锁消除。

锁消除的依据是逃逸分析的数据支持

  1. public class Test {
  2. public static void main(String[] args) {
  3. contactString("aa", "bb", "cc");
  4. }
  5. public static String contactString(String s1, String s2, String s3) {
  6. return new StringBuffer().append(s1).append(s2).append(s3).toString();
  7. }
  8. }

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的情况下,对锁进行消除。

锁粗化

下面这段代码,for循环中,会进出100次append同步方法,JVM就会将append方法是的锁消除,将锁加到for循环上,这要很多锁就变成了一个锁。

  1. public class Test {
  2. public static void main(String[] args) {
  3. StringBuffer sb = new StringBuffer();
  4. for(int i = 0; i < 100; i++) {
  5. sb.append("aa");
  6. }
  7. }
  8. }

锁粗化是指JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需加锁一次即可。

示例

JDK8
参数:-XX:BiasedLockingStartupDelay=0

  1. public class Test {
  2. final static Object LOCK = new Object();
  3. public static void main(String[] args) {
  4. System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
  5. Thread t1 = new Thread() {
  6. @Override
  7. public void run() {
  8. getLock();
  9. }
  10. };
  11. t1.setName("t1");
  12. t1.start();
  13. try {
  14. Thread.sleep(200);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. Thread t2 = new Thread() {
  19. @Override
  20. public void run() {
  21. getLock();
  22. }
  23. };
  24. t2.setName("t2");
  25. t2.start();
  26. }
  27. public static void getLock() {
  28. synchronized(LOCK) {
  29. System.out.println(Thread.currentThread().getName());
  30. System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
  31. }
  32. }
  33. }

在这里插入图片描述
将线程1、2中间的sleep()注释掉,两线程就会竞争锁,此时是重量级锁,并且可以看到MarkWord中有,除了锁标记(10)外,其余52个bit也是相等的,其中记录的就是锁对象关联的ObjectMonitor的地址:
在这里插入图片描述

  1. public class Test {
  2. final static Object LOCK = new Object();
  3. public static void main(String[] args) {
  4. System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
  5. System.out.println(Integer.toHexString(LOCK.hashCode()));
  6. System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
  7. getLock();
  8. }
  9. public static void getLock() {
  10. synchronized(LOCK) {
  11. System.out.println(Thread.currentThread().getName());
  12. System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
  13. }
  14. }
  15. }

在这里插入图片描述

synchronized小结

synchronized底层使用了monitorenter和monitorexit指令,每个锁对象都会关联一个monitor监视器,它有两个主要属性:owner记录当前拥有锁的线程,recursion记录当前锁被获取的次数。当执行到monitorexit时,recursion会减1,当它的值减到0时,这个线程就会释放锁。

synchronized和Lock的区别

synchronized和Lock都可以用来解决多线程安全问题,保证线程同步。区别是:

  • synchronized是关键字,Lock是接口,必须通过实例化一个实现了Lock锁的接口才能得到Lock锁对象,比如ReentrantLock
  • synchronized发生异常时,会自动释放锁;Lock锁必须通过调用unLock()去释放,因此可能造成死锁现象
  • synchronized不能让等待锁的线程中断;而Lock可以让等待锁的线程中断,就是通过调用它的tryLock()方法,如果调用的是lock()方法,则是不可中断的
  • synchronized无法知道线程是否成功获取锁,而Lock可以,当设置为可中断锁时,tryLock()方法返回布尔值代表是否获得锁
  • synchronized能锁住方法和代码块,加锁和释放锁是由JVM自动完成的,Lock只能锁住代码块,加锁和释放锁的时机由程序员自己决定
  • Lock可以使用读锁提高多线程的读效率,读锁:Lock的一个实现类ReentrantReadWriteLock,允许多个线程读,但只允许一个线程写
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。公平锁就是唤醒线程的时候,哪个线程先来就先唤醒那个,非公平锁是随机唤醒一个线程。可以给ReentrantLock的构造器传一个布尔值设定是否是公平锁。

平时写代码如何对synchronized优化

  • 减小synchronized的范围,同步代码块中代码执行时间尽量短
  • 降低synchronized锁的粒度
  • 读写分离,读取时不加锁,写入和删除时加锁

发表评论

表情:
评论列表 (有 0 条评论,96人围观)

还没有评论,来说两句吧...

相关阅读

    相关 synchronized原理

    ![4e33ae1c68c51a0a3dbc2deb7e141702.png][] 一.锁升级 synchronized相对比较‘智能’,会根据锁竞争的激烈程度进行锁升级,

    相关 20.synchronized原理详解

    在java中,最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 sync

    相关 Synchronize 原理

    1. 线程的状态 Thread.State 枚举,将线程划分为六中状态。 ![image-20210531180611460][] `new` 线程刚被创建,但是还没