【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 线程阻塞,响应时间缓慢 追求吞吐量 同步块执行速度较长


相关文章
|
10天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
11天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
8天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
10天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
26 2
|
11天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
4月前
|
存储 安全 Java
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
39 0
|
4月前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
59 0
|
6月前
|
安全 Java 编译器
Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字(一)
线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。 本文将对Java多线程编程中,线程安全问题展开详细的讲解。
101 0
|
6月前
|
安全 Java 调度
Java多线程- synchronized关键字总结
Java多线程- synchronized关键字总结
49 0
|
安全 Java 数据安全/隐私保护
Java基础进阶多线程-线程安全和synchronized关键字
Java基础进阶多线程-线程安全和synchronized关键字
Java基础进阶多线程-线程安全和synchronized关键字