Synchronized 分析

Synchronized 关键字

锁分类

锁分为乐观锁、悲观锁。

乐观锁的意义是认为读多写少,遇到并发的可能性低,每次去获取数据都认为没有其他线程去修改,所以不会上锁,只是再更新的时候判断在此期间是否有其他线程去修改了数据,一般采用的是先读出当前版本号,然后利用CAS去更新数据。

悲观锁则是认为写多读少,遇到并发的可能性高,每次去读写数据时,都会上锁。使得锁持有的时候,其他线程想要操作会被阻塞。而Synchronized则是悲观锁的具体实现。

Synchronized 持有锁其实是获取对象的锁。而Synchronized 锁又分为类锁和实例锁。类锁的范围包括类对象和类的实例对象。而实例锁只是实例对象。

主要形式以下三种:

  • 普通同步方法,锁的是当前实例对象。

  • 静态同步方法,锁的是当前类的class对象。

  • 同步方法块,锁的是Synchorinezd括号里配置的对象。

测试

  1. 加在对象方法前,锁住整个方法,这种使用方法,主要是用来锁住同一对象的,假设现在有两个不同的对象,调用这个方法,那么关键字synchornized关键字不会起到作用。

    private synchronized void syncMethod() throws Exception {
        for (int i = 0; i < 50; i++){
            count++;
            Thread.sleep(50);
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }
  2. synchronized关键字加在类方法前:因为synchronized加在类方法前,代表的意思是锁住整个类。在不同的线程中,调用同步方法是互斥的。

    private static void oneObjTestSyncStaticMethod() throws Exception {
        for (int i = 0; i < 2; i++) {
            Thread thread = new Thread();
            thread.start();
            syncStaticMethod1();
            syncStaticMethod2();
        }
    }
    ​
    private static synchronized void syncStaticMethod1() throws Exception {
        for (int i = 0; i < 50; i++){
            count++;
            Thread.sleep(50);
            System.out.println(Thread.currentThread().getName() + "1:" + count);
        }
    }
    ​
    ​
    private static synchronized void syncStaticMethod2() throws Exception {
        for (int i = 0; i < 50; i++){
            count++;
            Thread.sleep(50);
            System.out.println(Thread.currentThread().getName() + "2:" + count);
        }
    }
  3. 加在代码块里:锁的是Synchorinezd括号里配置的对象

    // 锁的是当前对象实例
    private static synchronized void syncStaticMethod1() throws Exception {
        synchronized(this) {
            for (int i = 0; i < 50; i++){
                count++;
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "1:" + count);
            }
        }
    }
    ​
    // 锁的是当前类
    private static synchronized void syncStaticMethod2() throws Exception {
        synchronized(xxx.class) {
            for (int i = 0; i < 50; i++){
                count++;
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "2:" + count);
            }
        }
    }

Synchronized 锁实现原理

对象结构

Synchronized 锁对象实例时,是通过对象的对象头实现的,下面就先介绍下对象实例结构。

image-20210414125054344

对象实例的结构:64位虚拟机前提下

  • 对象头:16个字节(开启指针压缩则为12个字节)

    • MarkWord:占8字节,存储对象自身运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。总共占64bit(64位虚拟机),下面会重点介绍。

    • klass:占8个字节, 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类实例。(开启指针压缩则为4个字节)

    • 数组长度:如果对象是数组的话,那么对象头中存在一块区域记录数组长度。占4个字节,数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+ 数组markword为4字节(64位未开启指针压缩的为8字节)) + 对齐4=16字节。

  • 对象(数组)实际数据:对象实例的有效信息。

  • 对齐填充:规定对象起始地址必须时8字节的整数倍,就是说对象的大小必须时8字节的整数倍,当整个对象的大小不是8字节的倍数,则会通过对其填充去补全。

锁升级过程

上面解释了Synchornized关键字主要的是通过对象头的MarkWord实现加锁过程。锁的升级过程大致分为4种状态。无锁->偏向锁->轻量级锁->重量级锁。

image-20210415091047314

  • 无锁:当一个对象刚开始new出来时,该对象是无锁状态。此时偏向锁位为0,锁标志位01

  • 偏向锁:当一个线程A访问临界区(同步块)时,如果获取锁成功,则会在对象头的MarkWord的线程ID修改为线程A的线程ID。

    • 如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程将永远不会触发同步。偏向锁在资源无竞争的情况下消除了同步语句,不会执行CAS操作。

    • 如果被其他线程访问,表示存在着线程竞争偏向锁。如果这个竞争线程为线程B,这个时候会使用CAS替换MarkWord的线程ID为线程B的线程ID。

      • 如果CAS成功,表示之前的线程A已经不存在,MarkWord的线程ID修改为线程B。

      • 如果CAS失败,表示之前线程仍然存在,那么暂停之前线程,设置线程偏向锁标志为0,锁标志为00,升级为轻量级锁。

    image-20210415120645951

    • 撤销偏向锁:等到竞争出现才会释放锁的机制,当升级为轻量级锁时,首先会撤销偏向锁。

  • 轻量级锁

    JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。

    • 自旋定义:在两个线程同时获取锁时,线程A和线程B就存在竞争,如果锁被线程B获取到,那么当前线程A就会采用等待线程B释放锁,然后再去获取锁。当前自旋的时候,需要设置限度。不然就会导致线程A一直等待占用CPU。如果超过时间没有获取到,那么就会将线程A挂起。

    • 加锁过程:

      • JVM会为每个线程在当前线程栈帧中创建用于存储锁记录的空间,称为Displaced MarkWord。如果一个线程获得锁的时候发现是轻量级锁,会把锁的MarkWord复制到当前线程的Displaced MarkWord里面。

      • 然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的地址指针。如果成功,当前线程获得锁。

      • 如果失败且当前线程已经持有了该锁,代表这是一次锁重入,设置Displaced MarkWord为null,起到一个重入计数器的作用,然后结束。

      • 如果失败且当前线程没有持有该锁,表示MarkWord已经被替换成了其他线程的锁记录,存在其他线程竞争,当前线程采取使用自旋来获取锁。自旋太久也不好,可以设置自旋次数。超过自旋次数还未获取到锁,则进入阻塞状态。同时这个锁被升级为重量级锁。

    • 释放锁过程:上述的CAS操作失败时,会释放锁并唤醒阻塞的线程。

    image-20210415122159783

  • 重量级锁

    重量级锁依赖操作系统的互斥量(mutex)实现,而操作系统中线程间状态的转换需要相对比较长的时间。所以效率低,但是阻塞的线程不会消耗CPU。

    • 重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

    • 当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到ContentionList的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将ContentionList中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

    • 如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。

image-20210415124059830

锁优缺点对比

image-20210415122354048

猜你喜欢

转载自blog.csdn.net/LarrYFinal/article/details/115721126