【Java并发编程】Synchronized关键字实现原理(二)

简介: 【Java并发编程】Synchronized关键字实现原理

4、Synchronized锁升级

在JDK1.6之前Synchronized只有重量级锁,没有获得锁的线程会阻塞,直到被唤醒才能再次获得锁,JDK1.6之后对锁做了很多优化引入了偏向锁、轻量级锁、重量级锁

4.1、无锁

public class Student {
    public static void main(String[] args) {
        Student stu=new Student();
        System.out.println("10机制hashCode:"+stu.hashCode());
        System.out.println("16机制hashCode:"+Integer.toHexString(stu.hashCode()));
        System.out.println("2机制hashCode:"+Integer.toBinaryString(stu.hashCode()));
        System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
}

两行Value里面存储了8个字节的Mark Word,相当于如下两行数据

01 f2 b2 8d (00000001 11110010 10110010 10001101) (-1917652479)
56 00 00 00 (01010110 00000000 00000000 00000000) (86)

这里实际上包含了同样结果的两种数据格式二进制和16进制,去掉括号外面的数据,两行整合成一行

二进制

(00000001 11110010 10110010 10001101)  (01010110 00000000 00000000 00000000)

16进制

01 f2 b2 8d  56 00 00 00

因为是小端存储,所以需要倒过来观看,数据顺序应该反过来

二进制

(00000000 00000000 00000000 01010110) (10001101 10110010 11110010 00000001)

16进制

00 00 00 56 8d b2 f2 01

56 8d b2 f2就是代表16机制hashCode

这样才是一个方便阅读的Mark Word结构,根据64位虚拟机的Mark Word结构示意图

最后三位为【001】,0代表偏向锁标记为,01表示锁标记

发现hashCode部分刚好等于打印出来的二进制HashCode:1010110 10001101 10110010 11110010,HashCode之所以不为空是因为调用了HashCode方法才显示出来

4.2、无锁升级偏向锁

public static void main(String[] args) {
        Student stu=new Student();
        System.out.println("=====加锁之前======");
        System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        synchronized (stu){
            System.out.println("=====加锁之后======");
            System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
}

按照上诉步骤找到锁标记

最后三位为【000】,其中最后两位是【00】,按照之前的存储状态定义这是轻量级锁,本身没有存在锁竞争很明显不对,原因是因为JVM开启了偏向锁延迟加载,我们启动程序的时候偏向锁还没开启,在程序启动时添加参数:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 //关闭偏向锁的延迟

再次查看程序运行结果

加锁之后锁标记是【101】表示偏向锁是符合预期的,但发现没加锁之前锁也就是偏向锁,本应该是无锁

发现没加锁之前没有线程ID,加锁之后才有线程ID, thread指针 和 epoch 都是0,说明当前并没有线程获得锁,表示可偏向的状态,所以无锁也是一个特殊的偏向锁,当有线程获取到时才会真正变为偏向锁

偏向锁的主要作用就是当同步代码块被一个线程多次访问,只有第一次访问的时候需要记录线程的ID,后续就会一直持有着锁而不需要再次加锁释放锁,因为只有一个线程那么该线程在后续多次访问就会自动获得锁,为了提高一个线程执行的性能,而不需要每次都去修改对象头的线程ID还有锁标志才能够获得锁

4.3、偏向锁流程

偏向锁获取流程:

  1. 首先查看Mark Word中的锁标记以及线程ID是否为空,如果锁标记是101代表是可偏向状态
  2. 如果是可偏向状态,再查看线程ID是当前的线程,直接执行同步代码块
  3. 如果是可偏向状态但是线程ID为空或者线程ID已被其他线程持有,那么就需要通过CAS操作去修改Mark Word中线程ID为当前线程还有锁标记,然后执行同步代码块
  4. CAS修改失败的话,就会开始撤销偏向锁,撤销偏向锁需要达到全局安全点,然后检查线程的状态
  5. 如果线程还存活检查线程是否在执行同步代码块中的代码,如果是升级为轻量级锁进行CAS竞争
  6. 如果没有线程存活,直接把偏向锁撤销到无锁状态,然后另一个线程会升级到轻量级锁

偏向锁撤销:

一种出现竞争出现才释放锁的机制,另外有线程来竞争锁,不能再使用偏向锁了,需要升级为轻量级锁,原来的偏向锁需要撤销,就会出现两种情况:

  1. 线程还没有执行完,其他线程就来竞争,导致需要撤销偏向锁,此时当前线程升级为持有轻量级锁,继续执行代码
  2. 线程执行完毕退出了同步代码块,将对象头设置为无锁并且撤销偏向锁重新偏向

偏向锁批量重偏向:

当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作,过程比较耗时,所以当撤销次数达到20次以上的时候,20这个值可以修改,会触发重偏向,直接把偏向锁偏向线程2

偏向锁就是一段时间内,只由一个线程来获得和释放锁,加锁的方式就是通过把线程ID保存到锁对象的Mark Word中

4.4、偏向锁升级轻量级锁

public class Student {
    public static void main(String[] args) {
        Student stu=new Student();
        System.out.println("=====加锁之前======");
        System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        synchronized (stu){
            System.out.println("=====加锁之后======");
            System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
        Thread thread=new Thread(){
            @Override
            public void run() {
                synchronized (stu){
                    System.out.println("====轻量级锁====");
                    System.out.println(ClassLayout.parseInstance(stu).toPrintable());
                }
            }
        };
        thread.start();
    }
}

很明显由特殊状态的无锁->偏向锁->轻量级锁

偏向锁在不影响性能的情况下获得了锁,这时候如果还有一个线程来获取锁,如果没有抢占到就会自旋一定的次数,这个次数可以通过JVM参数控制,抢占到了锁就不需要阻塞,轻量级锁也称为自旋锁

这个自旋也是有代价的,如果线程数过多,一直都在使用自旋抢占线程会浪费CPU性能,所以自旋的次数必须要有个限制,JDK1.6中默认是10次,JDK1.6之后使用的自适应自旋锁,意味着自旋的次数并不是固定的,而是根据同一个锁上次自旋的时间,如果很少自旋成功,那么下次会减少自旋的次数甚至不自旋,如果自旋成功,会认为下次也可以自旋成功,会增加自旋的次数

4.5、轻量级锁流程

轻量级锁获取流程:

  1. 一个线程进入同步代码块,JVM会给每一个线程分配一个Lock Record,官方称之为“Dispalced Mark Word”,用于存储锁对象的Mark Word,可以理解为缓存一样存储了锁对象
  2. 复制锁对象的Mark Word到Lock Record中去
  3. 使用CAS将锁对象的Mark Word替换为指向Lock Record的指针,如果成功表示轻量级锁占锁成功,执行同步代码块
  4. 如果CAS失败,说明当前lock锁对象已经被占领,当前线程就会使用自旋来获取锁

轻量级锁释放:

  1. 会把Dispalced Mark Word存储锁对象的Mark Word替换到锁对象的Mark Work中,会使用CAS完成这一步操作
  2. 如果CAS成功,轻量级锁释放完成
  3. 如果CAS失败,说明释放锁的时候发生了竞争触发锁膨胀,膨胀完之后调用重量级的释放锁方法

轻量级锁加锁的原理就是,JVM会为每一个线程分配一个栈帧用于存储锁的空间,里面有个Lock Record数据结构,也就是BaseObjectLock对象,会把锁对象里面的Mark Word复制到自己的BaseObjectLock对象里面,然后使用CAS把对象的Mark Word更新为指向Lock Record的指针,如果成功就获取锁,如果失败表示已经有其他线程获取到了锁,然后继续使用自旋来获取锁

轻量级锁每次都需要释放锁,而偏向锁只有存在竞争的时候才释放锁为了避免反复切换

4.6、轻量级锁升级重量级锁

package com.ylc;
import org.openjdk.jol.info.ClassLayout;
public class Student {
    public static void main(String[] args) {
        Student stu=new Student();
        System.out.println("=====加锁之前======");
        System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        synchronized (stu){
            System.out.println("=====加锁之后======");
            System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
        Thread thread=new Thread(){
            @Override
            public void run() {
                synchronized (stu){
                    System.out.println("====轻量级锁====");
                    System.out.println(ClassLayout.parseInstance(stu).toPrintable());
                }
            }
        };
        thread.start();
        for (int i=0;i<3;i++){
            new Thread(()->{
                synchronized (stu){
                    System.out.println("====重量级锁====");
                    System.out.println(ClassLayout.parseInstance(stu).toPrintable());
                }
            }).start();
        }
    }
}

锁的标志为【010】代表重量级锁

倘若通过自旋重试一定次数还获取不到锁,那么就只能阻塞等待线程唤醒了,最后升级为重量级锁

4.7、重量级锁流程

重量级锁获取流程

  1. 首先会进行锁膨胀
  2. 然后会创建一个ObjectMonitor对象,通过该把该对象的指针保存到锁对象里面
  3. 如果获取锁失败或者对象本身就处于锁定状态,会进入阻塞状态,等待CPU唤醒线程重新竞争锁
  4. 如果对象无锁就会获取锁

重量级锁释放流程

  1. 会把ObjectMonitor中的的持有锁对象owner置为null
  2. 然后从阻塞队列里面唤醒一个线程
  3. 唤醒的线程重新竞争锁,如果没有抢占到继续等待

由此可以发现Synchronized底层的锁机制是通过JVM层面根据线程竞争情况来实现的

5、Synchronized锁消除

Java虚拟机在JIT编译时会去除没有竞争的锁,消除没有必要的锁,可以节省锁的请求时间

public class Student {
    public static void main(String[] args) {
       for (int i=0;i<=10;i++){
           new Thread(()->{
               Student.lock();
           }).start();
        }
    }
      public  static void lock(){
        Object o=new Object();
            synchronized (o){
                System.out.println("hashCode:"+o.hashCode());
            }
        }
}

每次都加了锁,可是都不是同一把锁,无法产生竞争这样的锁没有意义,相当于会无视synchronized (o)的存在

6、Synchronized锁粗化

锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁

public class Student {
    public static void main(String[] args) {
        Object o=new Object();
        new Thread(()->{
            synchronized (o){
                System.out.println("加一次锁");
            }
            synchronized (o){
                System.out.println("加两次锁");
            }
            synchronized (o){
                System.out.println("加三次锁");
            }
            synchronized (o){
                System.out.println("加四次锁");
            }
        }).start();
    }
}

把小锁范围扩大,优化后变成

public class Student {
    public static void main(String[] args) {
              Object o=new Object();
        new Thread(()->{
            synchronized (o){
                System.out.println("加一次锁");
            }
        }).start();
    }
}

7、锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法仅有纳米级的差距 如果线程间存在锁的竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问的同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的相应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间 同步响应非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量 同步块执行速度较长


目录
打赏
0
0
0
0
19
分享
相关文章
k8s的出现解决了java并发编程胡问题了
Kubernetes通过提供自动化管理、资源管理、服务发现和负载均衡、持续交付等功能,有效地解决了Java并发编程中的许多复杂问题。它不仅简化了线程管理和资源共享,还提供了强大的负载均衡和故障恢复机制,确保应用程序在高并发环境下的高效运行和稳定性。通过合理配置和使用Kubernetes,开发者可以显著提高Java应用程序的性能和可靠性。
48 31
注解的艺术:Java编程的高级定制
注解是Java编程中的高级特性,通过内置注解、自定义注解及注解处理器,可以实现代码的高度定制和扩展。通过理解和掌握注解的使用方法,开发者可以提高代码的可读性、可维护性和开发效率。在实际应用中,注解广泛用于框架开发、代码生成和配置管理等方面,展示了其强大的功能和灵活性。
52 25
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
课时6:Java编程起步
课时6:Java编程起步,主讲人李兴华。课程摘要:介绍Java编程的第一个程序“Hello World”,讲解如何使用记事本或EditPlus编写、保存和编译Java源代码(*.java文件),并解释类定义、主方法(public static void main)及屏幕打印(System.out.println)。强调类名与文件名一致的重要性,以及Java程序的编译和执行过程。通过实例演示,帮助初学者掌握Java编程的基本步骤和常见问题。
|
15天前
|
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
39 21
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
49 17
Java HashMap详解及实现原理
Java中的this关键字详解:深入理解与应用
本文深入解析了Java中`this`关键字的多种用法
201 9
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
4月前
|
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
52 1
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
67 3

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等