synchronized读书笔记

本文详细解读了synchronized关键字在实例方法、静态方法和代码块中的应用,探讨了Java对象内存布局、Monitor对象和锁的优化,包括轻量级锁、偏向锁及其性能提升策略。

1. 认识synchronized

synchronized 是 Java 并发模块 非常重要的关键字,它是 Java 内建的一种同步机制,
代表了某种内在锁定的概念当一个线程对某个共享资源加锁后,其他想要获取共享资源的线程必须进行等待.
synchronized 也具有互斥和排他的语义。通过独占某个资源以达到互斥、排他的目的。

2. synchronized使用

  • synchronized 修饰实例方法,相当于是对类的实例进行加锁, 即锁是new出来的当前实例对象
  • synchronized 修饰静态方法,相当于是对类对象(即类的class对象)进行加锁
  • synchronized 修饰代码块,相当于是给对象进行加锁(锁是括号里面的对象),在进入代码块前需要先获得对象的锁。

当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码

2.1 修饰实例方法(普通方法)

synchronized 修饰实例方法,实例方法是属于类的实例。
synchronized 修饰的实例方法相当于是对象锁。

public synchronized void method(){ }
public class TSynchronized implements Runnable{
    static int i = 0;
    public synchronized void increase(){
        i++;
        System.out.println(Thread.currentThread().getName());
    }
    @Override
    public void run() {
        for(int i = 0;i < 1000;i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        //thread.join()表示等待这个线程处理结束。
        //这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性。
        aThread.join();
        bThread.join();
        System.out.println("i = " + i);
    }
}
//result:上面输出的结果 i = 2000 ,并且每次都会打印当前线程的名字

解释: ,代码中的 i 是一个静态变量,静态变量也是全局变量,静态变量存储在方法区中。
increase 方法由 synchronized 关键字修饰,但是没有使用 static 关键字修饰,表示 increase 方法是一个实例方法,每次创建一个 TSynchronized 类的同时都会创建一个 increase 方法,increase 方法中只是打印出来了当前访问的线程名称。
Synchronized 类实现了 Runnable 接口,重写了 run 方法,run 方法里面就是一个 0 - 1000 的计数器,这个没什么好说的。
在 main 方法中,new 出了两个线程,分别是 aThread 和 bThread,Thread.join 表示等待这个线程处理结束。这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性

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(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

在这里插入图片描述
结果: 线程2需要等待线程1的method1执行完成才能开始执行method2方法

2.2 修饰静态方法

synchronized 修饰静态方法就是 synchronized 和 static 关键字一起使用
当 synchronized 作用于静态方法时,表示的就是当前类的锁,
因为静态方法是属于类的,它不属于任何一个实例成员,因此可以通过 class 对象控制并发访问。

public static synchronized void increase(){}

这里需要注意一点,因为 synchronized 修饰的实例方法是属于实例对象, 而 synchronized
修饰的静态方法是属于类对象,所以调用 synchronized 的实例方法并不会阻止访问 synchronized 的静态方法。

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(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method2();
            }
        }).start();
    }
}

执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法)。
所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1和method2,不能并发执行。
在这里插入图片描述

2.3 修饰代码块

下面代码中将 obj 作为锁对象对其加锁,每次当线程进入 synchronized 修饰的代码块时就会要求当前线程持有obj 实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。
synchronized 修饰的代码块,除了可以锁定对象之外,也可以对当前实例对象锁、class 对象锁进行锁定

public void run() {
    synchronized(obj){
        for(int j = 0;j < 1000;j++){
            i++;
        }
    }
}
    // 实例对象锁
    synchronized(this){
        for(int j = 0;j < 1000;j++){
            i++;
        }
    }

    //class对象锁
    synchronized(TSynchronized.class){
        for(int j = 0;j < 1000;j++){
            i++;
        }
    }
ublic 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(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

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

3. 浅析原理

同步代码块是使用monitorenter和monitorexit指令实现的,
同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。
先需要了解两个重要的概念:Java对象头,Monitor。

3.1 Java对象内存布局

可先阅读该篇文章JVM中对象描述
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

这三块区域的内存分布如下图所示:
在这里插入图片描述

3.2对象头 Header

synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?
Hotspot虚拟机的对象头主要包括三部分数据:

  • Mark Word(标记字段) 存对象的hashCode或锁信息
  • Klass Pointer(类型指针)存储 到对象类型数据的指针
  • Array length 数组长度(只有当前对象是数组才有)

Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键
注:下面的内容指的是64位的VM

在这里插入图片描述
在这里插入图片描述
在上面的虚拟机对象头分配表中,我们可以看到有几种锁的状态:无锁(无状态),偏向锁,轻量级锁,重量级锁,其中轻量级锁和偏向锁是 JDK1.6 中对 synchronized 锁进行优化后新增加的,其目的就是为了大大优化锁的性能,所以在 JDK 1.6 中,使用 synchronized 的开销也没那么大了。其实从锁有无锁定来讲,还是只有无锁和重量级锁,偏向锁和轻量级锁的出现就是增加了锁的获取性能而已,并没有出现新的锁。
从图中可以看到,在重量级锁时,对象头的锁标记位为 10,并且会有一个指针指向这个 monitor 对
象,所以锁对象和 monitor 两者就是这样关联的。(下方会介绍几种锁的区别,但monitor是与重量级锁关联, 所以下面介绍monitor的锁时即指重量级锁)
而这个 monitor 在 HotSpot 中是 c++ 实现的,叫 ObjectMonitor,它是管程的实现,也有叫监视器的。

啥是 Monitor 对象呢?

3.3 Monitor 对象(重量级锁时) (翻译叫监视器或者管程)

任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制。 管程是一种同步原语,在 Java 中指的就是 synchronized,可以理解为 synchronized 就是 Java 中对管程的实现。
管程提供了一种排他访问机制,这种机制也就是 互斥。互斥保证了在每个时间点上,最多只有一个线程会执行同步方法。
所以你可以理解为 Monitor 对象其实就是使用管程控制同步访问的一种对象。
下图是monitor对象包含的内容: 先了解一下概念
在这里插入图片描述
这段 C++ 中需要注意几个属性:_cxp、 _WaitSet 、 _EntryList 和 _Owner,每个等待获取锁的线程都会被封装称为 ObjectWaiter 对象。
线程在任何时间最多出现在一个列表上:要么是cxp, 要么在EntryList要么在waitSet

  • _cxp: 多个线程争抢锁会先到这个列表中(多线程竞争锁进入时的单向链表)
  • _EntryList : 这个列表也是存争抢锁失败的锁
  • _WaitSet: 调用wait()方法的线程, 也就是说处于wait状态的线程 会存在这个里面
  • _owner: 标识拥有锁的线程,若无则为null
  • _count: 线程获取锁的次数
  • _recursions :线程的重入次数
  • _object: 存储的锁对象(比如synchronized括号里的对象)

为什么会有_cxq 和 _EntryList 两个列表来放线程?

因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 存住这些并发,然后另外搞一个 _EntryList 这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。
在这里插入图片描述

3.4 Synchronized 修饰代码块时

此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,enter 就是要获得锁了,exit 就是要解
锁了,与之对应的就是获得锁和解锁。
修饰代码块
字节码
从上图来看,执行 System.out 之前执行了 monitorenter 执行,这里执行争锁动作,拿到锁即可进入调用完之后有个 monitorexit 指令,表示释放锁
图中还标了一个 monitorexit 指令时,因为有异常的情况也需要解锁,不然就死锁了
从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁?
编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了。

monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

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

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

monitorenter指令(争锁)
在这里插入图片描述
monitorexit指令(解锁)

在这里插入图片描述

调用 wait 的方法
就是将当前线程加入到 _waitSet 这个双向链表中,
然后再执行ObjectMonitor::exit 方法来释放锁。

调用 notify 的方法
就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。
引入自旋
synchronized 的原理大致应该都清晰了,我们也知道了底层会用到系统调用,会有较大的开销,那思考一下该如何优化?
从小标题就已经知道了,方案就是自旋
自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放。
正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。
所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。
但是自旋的次数又是一个难点,在竞争很激烈的情况,自旋就是在浪费 CPU,因为结果肯定是自旋一会让之后阻塞。
所以 Java 引入的是自适应自旋,根据上次自旋次数,来动态调整自旋的次数,这就叫结合历史经验做事。
注意这是重量级锁的步骤,别忘了前面开头说的。

总结:

synchronized 底层是利用 monitor 对象,CAS 和 mutex 互斥锁来实现的,内部会有等待队列(cxq 和 EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。
未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。
然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。
所以又引入了自适应自旋机制,来提高锁的性能。

3.5 Synchronized 修饰方法时

修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。
此时字节码中没有 monitorenter 和 monitorexit 指令,不过在当前方法的访问标记上做了手脚
原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分
这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。

3.6 锁优化

在这里插入图片描述
我们再思考一下,是否有这样的场景:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。
在锁竞争不激烈的情况下,这种场景还是很常见的,可能是常态,所以轻量级锁的引入很有必要。
在介绍轻量级锁的原理之前,再看看之前 MarkWord 图。
在这里插入图片描述

3.6.1 轻量级锁

轻量级入锁操作的就是对象头的 MarkWord 。

  • 如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw里。
    然后通过 CAS 把锁对象头指向这个 LockRecord 。
  • 如果当前是有锁状态,并且是当前线程持有的,则将 null 放到 dhw 中,这是重入锁的逻辑。
    在这里插入图片描述
    轻量级解锁操作
    就是要把当前栈帧中 LockRecord 存储的 markword (dhw)通过 CAS 换回到对象头中。
    如果获取到的 dhw 是 null 说明此时是重入的,所以直接返回即可,否则就是利用 CAS 换,如果 CAS 失败说明此时有竞争,那么就膨胀!

注: 每次加锁肯定是在一个方法调用中,而方法调用就是有栈帧入栈,如果是轻量级锁重入的话那么此时入栈的栈帧里面的 dhw 就是null,否则就是锁对象的 markword。
这样在解锁的时候就能通过 dhw 的值来判断此时是否是重入的。

3.6.2 偏向锁

场景一开始一直只有一个线程持有这个锁,也不会有其他线程来竞争,此时频繁的 CAS 是没有必要的,CAS 也是有开销的。
所以 JVM 研究者们就搞了个偏向锁,就是偏向一个线程,那么这个线程就可以直接获得锁。
我们再看看这个图,偏向锁在第二行
在这里插入图片描述
如果当前锁对象支持偏向锁,那么就会通过 CAS 操作:将当前线程的地址(也当做唯一ID)记录到 markword 中,并且将标记字段的最后三位设置为 101。

之后有线程请求这把锁,只需要判断 markword 最后三位是否为 101,是否指向的是当前线程的地址。
还有一个可能很多文章会漏的点,就是还需要判断 epoch 值是否和锁对象的类中的 epoch 值相同。
如果都满足,那么说明当前线程持有该偏向锁,就可以直接返回。

这 epoch 干啥用的?

可以理解为是第几代偏向锁。 偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。 而当一类对象撤销的次数过多,比如有个
Tets 类的对象作为偏向锁,经常被撤销,次数到了一定阈值 (XX:BiasedLockingBulkRebiasThreshold,默认为
20 )就会把当代的偏向锁废弃,把类的 epoch 加1

所以当类对象和锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。
不过为保证正在执行的持有锁的线程不能因为这个而丢失了锁,偏向锁撤销需要所有线程处于安全点,然后遍历所有线程的 Java栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch值加 1。
当撤销次数超过另一个阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为40),则废弃此类的偏向功能,也就是说这个类都无法偏向了。

3.7 小总结

轻量级锁与偏向锁类似,都是jdk对于多线程的优化,不同的是轻量级锁是通过CAS来避免开销较大的互斥操作,而偏向锁是在无资源竞争的情况下完全消除同步。
轻量级锁的“轻量”是相对于重量级锁而言的,它的性能会稍好一些。轻量级锁尝试利用CAS,在升级为重量级锁之前进行补救,目的是为了减少多线程进入互斥,当多个线程交替执行同步块时,jvm使用轻量级锁来保证同步,避免线程切换的开销,不会造成用户态与内核态的切换。但是如果过度自旋,会引起cpu资源的浪费,这种情况下轻量级锁消耗的资源可能反而会更多。

引入偏向锁的目的:
在只有单线程执行情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取及释放依赖多次CAS
原子指令,而偏向锁只依赖一次CAS原子指令置换ThreadID,之后只要判断线程ID为当前线程即可,
偏向锁使用了一种等到竞争出现才释放锁的机制,消除偏向锁的开销还是蛮大的。
如果同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的,可以通过-
XX:-UseBiasedLocking=false来关闭

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和核心态转换),但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁重入:对于不同级别的锁都有重入策略,偏向锁:单线程独占,重入只用检查threadId等于该线程;轻量级锁:重入将栈帧中lock record的header设置为null,重入退出,只用弹出栈帧,直到最后一个重入退出CAS写回数据释放锁;重量级锁:重入_recursions++,重入退出_recursions–,_recursions=0时释放锁

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值