icode icode
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

iqqcode

保持对技术的探索实践与热爱
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaSE

    • 面向对象

    • 常用API

    • 集合类

    • 多线程

      • Thread

        • 进程与线程
        • 黄牛卖票和模拟龟兔赛跑问题分析
        • 线程常用方法
        • 线程停止及守护线程
        • synchronized关键字解决线程安全问题
        • synchnized原理及优化
          • 1. synchronized的特点
          • 2. 对象锁monitor机制
            • 对象在内存中的存储
          • 3. synchronized底层原理
            • synchronized修饰同步代码块
            • synchronized修饰同步方法
            • monitorenter---monitorexit
            • 可重入锁
          • 4. JDK1.6后对synchronized的优化
            • 锁升级的过程
            • 偏向锁
            • 轻量级锁
            • 重量级锁
            • 锁粗化
            • 锁消除
          • 5. 用synchronized还是Lock
        • 线程间的通信
        • 死锁
        • Lock体系
        • ReentrantLock
        • 线程池使用
        • 线程池原理及配置
        • Java线程池的四种用法与使用场景
        • ThreadLocal
      • JUC

    • JDK8新特性

    • IO流

    • JVM

  • JavaWeb

  • JavaEE

  • JavaTopic

  • 设计模式

  • 计算机基础

  • Java后端
  • JavaSE
  • 多线程
  • Thread
iqqcode
2021-06-17
目录

synchnized原理及优化

sychronized的使用场景:

在这里插入图片描述

# 1. synchronized的特点

  1. 有序性:禁止指令重排
  2. 可见性:JMM内存模型
  3. 原子性:加锁
  4. 可重入性:synchronized锁计数器
  5. 不可中断性:锁不可被中断(区别于Lock的tryLock)
  6. 非公平:Lock可实现公平

# 2. 对象锁monitor机制

# 对象在内存中的存储

image-20201029091926286

【对象头】 对象为8byte

  • Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。他会根据对象的状态-复用自己的存储空间,Mark Word中的数据会随着标志位的变化而变化
  • Klass Point(类型指针):对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

【实例数据】:存放类的数据信息,父类的信息

【对齐填充数据】:虚拟机要求对象起始地址必须是8字节的整数倍

image-20200820161201862

  • 刚开始Monitor中Owner为null

  • 当Thread-2执行synchronized(obj)就会成为Monitor的所有者,Owner置为Thread-2,Monitor中只能有一个Owner

  • 在Thread-2上锁的过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED

  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁;竞争的时是非公平的,并不是先进入EntryList中得线程先获取锁,取决于操作系统调度器的调度顺序

  • 图中WaitSet中的Thread-0、Thread-1是已经获得锁,但条件不满足进入WAITING状态的线程

注意

  • synchronized必须是进入同一个对象的monitor才有上述的效果

  • 不加synchronized的对象不会关联监视器,不遵从以上规则


# 3. synchronized底层原理

# synchronized修饰同步代码块

先来看一段简单的代码 在这里插入图片描述

为了解到synchronized的底层实现原理,我们来对这段代码进行反编译

先通过cd命令进入到src下的package目录

先编译javac Test.java,生成class文件

反编译命令:

javap -c -v Test.class

然后我们来看同步代码块下生成的字节码:

在这里插入图片描述

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2     // <- lock引用<synchronized开始>          
         3: dup
         4: astore_1             // lock引用,存入局部变量表slot_1
         5: monitorenter         // 将lock对象 Markward置为 Monitor指针
         6: getstatic     #3                  
         9: ldc           #4                  
        11: invokevirtual #5                 
        14: aload_1
        15: monitorexit          // 将lock对象MarkWard重置,唤醒EntryList 
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit          // 将lock对象 Markward重置,唤醒 EntryList
        22: aload_2
        23: athrow
        24: return
      Exception table:             // 异常监测表
         from    to  target type   // 监测范围
             6    16    19   any
            19    22    19   any
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

执行同步代码块后首先要先执行monitorenter指令,退出的时候执行monitorexit指令。

通过分析之后可以看出,使用synchronized进行同步,其关键就是要获取对象的监视器monitor,当线程获取monitor后才能继续往下执行,否则就只能等待。

而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。

上述字节码中包含一个monitorenter指令以及两个monitorexit指令。这是因为JVM需要确保获得的锁在正常执行路径、异常执行路径上都能够被解锁。

# synchronized修饰同步方法

在这里插入图片描述 当用synchronized标记方法时,字节码中方法的访问标记flags多了 ACC_SYNCHRONIZED。

  • 进入该方法时,JVM需要进行 monitorenter 操作
  • 退出该方法时,不管是正常返回,还是向调用者抛异常,JVM均需要进行 monitorexit 操作

# monitorenter---monitorexit

  • 关于monitorenter 和 monitorexit 的作用,我们可以抽象地理解为:每个锁对象拥有一个锁计数器和一个 “指向持有该锁的线程” 的指针(对象头)

  • 当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。在这个情况下JVM会将该锁对象的持有线程设置为当前线程,并且将其计数器加1

  • 在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器再加1;否则需要等待,直至持有线程释放该锁

  • 当执行monitorexit时,JVM则需将锁对象的计数器减1。当计数器减为0时,便代表该锁已经被释放掉了


# 可重入锁

当执行monitorenter时,对象的monitor计数器值不为0,但是持有锁的线程恰好是当前线程。此时将monitor计数器值再次+1,当前线程再次进入同步方法或代码块

之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁

【证明锁的可重入与互斥】

synchronized修饰的test1方法中调用test2方法

class Sync implements Runnable {

    @Override
    public void run() {
        test1();
        test2();
    }

    public synchronized void test1() {
        if (Thread.currentThread().getName().equals("A")) {
            test2();
        }
    }

    public synchronized void test2() {
        if (Thread.currentThread().getName().equals("B")) {
            System.out.println("B线程进入该同步方法test2()...");
        }else {
            //此时B线程还没有启动
            System.out.println(Thread.currentThread().getName() + "线程--->进入test2()方法");
        }
    }
}

public class Reentrant {
    public static void main(String[] args) throws InterruptedException {
        Sync run = new Sync();
        new Thread(run,"A").start();
        //有时间差,保证A先启动
        Thread.sleep(2000);
        new Thread(run,"B").start();
    }
}
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

在这里插入图片描述

如果一个类中拥有多个synchronized方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作


# 4. JDK1.6后对synchronized的优化

synchronized的操作都是互斥的,Monitor机制是由操作系统来提供的,效率低。

当一个线程拿到了锁资源之后,因为要保证同步,所以其他线程只能等待该线程释放锁,效率自然降低。

优化的思想:让每个线程通过同步代码块时的速度提高

# 锁升级的过程

面:synchronized是个重量级锁,那它的优化有了解嘛?

应:为了减少获得锁和和释放锁带来的性能损耗引入了偏向锁、轻量级锁、重量级锁来进行优化,锁升级的过程如下:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

image-20201029093856354

首先是一个无锁的状态,当线程进入同步代码块的时候,会检查对象头内和栈帧中的锁记录-是否是-存入当前线程的ID。如果没有,则使用**CAS **进行替换。对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。

【偏向锁】

以后该线程进入和退出同步代码块不需要进行CAS 操作来加锁和解锁,只需要判断对象头的Mark word内是否存储指向当前线程的偏向锁。如果有表示已经获得锁,如果没有或者不是,则需要使用CAS进行替换;如果设置成功则当前线程持有偏向锁,反之将偏向锁进行撤销并升级为轻量级锁。

偏向锁不会释放锁(大华被问到过,答错了…)

6140

【轻量级锁加锁过程】

线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录(Displaced Mark Word)中,然后线程尝试使用CAS 将对象头中的Mark Word替换为指向锁记录的指针。

  • 如果成功,当前线程获得锁,
  • 反之表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁

【自旋-防止上下文切换】

Linux系统的用户态和内核态的切换很耗资源,其实就是线程的等待唤起过程。

为了减少开销,提高效率,会短暂的自旋防止线程被切换挂起!

自旋,过来的线程在不断自旋,防止线程被挂起。

  • 一旦可以获取资源,就直接尝试成功,

  • 直到超出阈值仍然没获取到,自旋锁的默认大小是10次,自旋都失败了。那就升级为重量级的锁,像1.5的一样,等待唤起咯。

-XX:PreBlockSpin可以修改

1640

【重量级锁】

当前线程获取到锁,其他线程处于阻塞中


# 偏向锁

JDK 1.6 之后默认synchronized

最乐观的锁:进入同步块或同步方法始终是一个线程

在不同时刻时,当出现另一个线程也尝试获取锁,偏向锁会升级为轻量级锁


# 轻量级锁

  • 不同时刻有不同的线程获取锁,基本不存在锁竞争
  • 同一时刻,如果不同线程尝试获取锁,会将偏向锁自动升级为重量级锁

# 重量级锁

  • JDK 1.6 之前的锁都是重量级锁,将线程阻塞挂起

  • 锁只有升级过程,没有降级


# 锁粗化

将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁

比如使用StringBuffer中的apperd方法来添加字符串

sb.append("a");
sb.append("b");
sb.append("c");
1
2
3

这里每次调用append方法都需要加锁和解锁操作

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


# 锁消除

当对象不属于共享资源时,对象内部的同步方法或同步代码块的锁会被自动解除

​

# 5. 用synchronized还是Lock

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

两者一个是JDK层面的一个是JVM层面的,我觉得最大的区别其实在,我们是否需要丰富的api,还有一个我们的场景。

比如我现在是滴滴,我早上有打车高峰,我代码使用了大量的synchronized,有什么问题?锁升级过程是不可逆的,过了高峰我们还是重量级的锁,那效率是不是大打折扣了?这个时候你用Lock是不是很好?

场景是一定要考虑的,我现在告诉你哪个好都是扯淡,因为脱离了业务,一切技术讨论都没有了价值。

编辑 (opens new window)
上次更新: 2021/06/27, 10:49:09
synchronized关键字解决线程安全问题
线程间的通信

← synchronized关键字解决线程安全问题 线程间的通信→

最近更新
01
匿名内部类
10-08
02
函数式接口
10-08
03
ARouter-Kotlin踩坑
10-05
更多文章>
Theme by Vdoing | Copyright © 2021-2023 iqqcode | MIT License | 备案号-京ICP备2021028793号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×