Synchronized

1、synchronized 简介

Synchronized 是一种互斥锁,也成为同步锁,它的作用是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以到达保证并发安全效果。在 JDK 1.6 以前,很多人称之为重量级锁,性能不高。但是在 JDK 1.6 以后,对 sychronized 进行了一些优化,引入了偏向锁,轻量级锁,以及重量级锁。synchrionized 会根据线程的竞争程度对锁进行升级和降级。

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:可保证一个线程的变化 (主要是共享数据的变化)被其他线程所看到
  • 有序性:有效解决重排序问题,即 “一个 unlock 操作先行发生 (happen-before)于后面对同一个锁的 lock 操作”
1.1 重量级锁

重量级锁是最传统的锁形式,它适用于多个线程竞争同步代码块的情况。当多个线程同时访问同步块时,它们将会进入阻塞状态,等待锁的释放。当锁被释放时,等待的线程会通过竞争获取锁,只有一个线程能成功,其他等待的线程会继续阻塞。因此,重量级锁的效率不高。在 JDK1.6 以前,synchronized 关键字就是重量级锁。

1.2 偏向锁

一个对象 A 刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程 T1来访问它的时候,它会偏向该线程,此时,对象 A 持有偏向锁

1.3 轻量锁

在偏向锁的基础上,如果有第二个线程 T2访问对象 A,T 2 会看到对象 A 处于偏向状态,代表在 A 存在竞争,然后检查原来持有检查原来持有该对象锁的线程 T 1 是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

1.4 自旋锁

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗 cpu 的,说白了就是让 cpu 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。即升级为重量锁。

1.5 几个锁的发展顺序

偏向锁—>轻量锁—>自旋锁—>重量锁

2、synchronized 原理

在分析原理前,先了解一下在 JVM 中,对象的存储状态。
对象存储在堆中,主要分为三部分内容:对象头,对象实例数据,对齐填充

2.1 对象头的存储形式


synchronized 与对象头中的 markword 字段有关
而对象头由如下两个部分组成:

1、Mark Word:存储的是对象的 hashCode、锁信息、或分代年龄、GC 标注等信息。
2、Class Metadata Address: 存储对象所属类 (元数据) 的指针,JVM 通过这个确定这个对象属于哪个类。

2.2 案例分析
public class Demo01 {
    public static void main(String[] args) {
        Runnable sellTicket = new Runnable() {
            @Override public void run() {
                synchronized (Demo01.class) {
                    System.out.println("我是run"); 
                    test01();
                } 
            }
            public void test01() { 
                synchronized (Demo01.class) {
                    System.out.println("我是test01")
                }
            } 
        };
        new Thread(sellTicket).start();
    } 
}

(1)同步代码块
通过 javap -p -v -c 对 class 字节码文件进行反编译,出现下面效果:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code: 
   stack=2, locals=4, args_size=1
   0: iconst_0
              1: istore_1
              2: getstatic #2 // Field obj:Ljava/lang/Object;
              5: dup 6: astore_2 
              7: monitorenter //获取锁时
              8: iinc 1, 1 
              11: aload_2
              12: monitorexit //释放锁时
              13: goto 21 
              16: astore_3 
              17: aload_2 
              18: monitorexit //发生异常会自动释放锁
              19: aload_3 
              20: athrow 
              21

每一个对象都会和一个监视器 monitor 关联。监视器被占用时会被锁住,其他线程无法来获取该 monitor。当 JVM 执行某个线程的某个方法内部的 monitorenter 时,它会尝试去获取当前对象对应的 monitor 的所有权。其过程如下:
若 monior 的进入数为 0,线程可以进入 monitor,并将 monitor 的进入数置为 1。当前线程成为 monitor 的 owner(所有者);
若线程已拥有 monitor 的所有权,允许它重入 monitor,则进入 monitor 的进入数加1 ;
若其他线程已经占有 monitor 的所有权,那么当前尝试获取 monitor 的所有权的线程会被阻塞,直到 monitor 的进入数变为 0,才能重新尝试获取 monitor 的所有权。
Monitorenter:
Synchronized 的锁对象会关联一个 monitor, 这个 monitor 不是我们主动创建的, 是 JVM 的线程执行到这个同步代码块, 发现锁对象没有 monitor 就会创建 monitor, monitor 内部有两个重要的成员变量 owner: 拥有这把锁的线程, recursions 会记录线程拥有锁的次数, 当一个线程拥有 monitor 后其他线程只能等待。
Monitorexit:
能执行 monitorexit 指令的线程一定是拥有当前对象的 monitor 的所有权的线程。执行 monitorexit 时会将 monitor 的进入数减 1。当 monitor 的进入数减为 0 时,当前线程退出 monitor,不再拥有 monitor 的所有权,此时其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。同时,monitorexit 插入在方法结束处和异常处,JVM 保证每个 monitorenter 必须有对应的 monitorexit。
总结:synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor (monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因)的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执⾏monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值