深入解析 Java 中的 Synchronized:原理、实现与性能优化

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 深入解析 Java 中的 Synchronized:原理、实现与性能优化

Synchronized 介绍

概念:synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。

线程的执行:代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的 wait 系列方法时释放该内置锁。

Synchronized 的三种使用方式

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础。synchronized 的三种使用方式如下:

普通同步方法(实例方法)

锁是当前实例对象 ,进入同步代码前要获得当前实例的锁。为了更加深刻的体会 synchronized 作用于实例方法的使用,我们先来设计一个场景,并根据要求,通过代码的实例进行实现。


场景设计

  1. 创建两个线程,分别设置线程名称为 threadOne 和 threadTwo;
  2. 创建一个共享的 int 数据类型的 count,初始值为 0;
  3. 两个线程同时对该共享数据进行增 1 操作,每次操作 count 的值增加 1;
  4. 对于 count 数值加 1 的操作,请创建一个单独的 increase 方法进行实现;
  5. increase 方法中,先打印进入的线程名称,然后进行 1000 毫秒的 sleep,每次加 1 操作后,打印操作的线程名称和 count 的值;
  6. 运行程序,观察打印结果。


结果预期:因为 increase 方法有两个打印的语句,不会出现 threadOne 和 threadTwo 的交替打印,一个线程执行完 2 句打印之后,才能给另外一个线程执行。

public class DemoTest extends Thread {
    //共享资源
    static int count = 0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase() throws InterruptedException {
      sleep(1000);
      count++;
      System.out.println(Thread.currentThread().getName() + ": " + count);
  }
    @Override
    public void run() {
        try {
            increase();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        DemoTest test = new DemoTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.setName("threadOne");
        t2.setName("threadTwo");
        t1. start();
        t2. start();
    }

结果验证

threadTwo 获取到锁,其他线程在我执行完毕之前,不可进入。
threadTwo: 1
threadOne 获取到锁,其他线程在我执行完毕之前,不可进入。
threadOne: 2

从结果可以看出,threadTwo 进入该方法后,休眠了 1000 毫秒,此时线程 threadOne 依然没有办法进入,因为 threadTwo 已经获取了锁,threadOne 只能等待 threadTwo 执行完毕后才可进入执行,这就是 synchronized 修饰实例方法的使用。


Tips:仔细看 DemoTest test = new DemoTest () 这就话,我们创建了一个 DemoTest 的实例对象,对于修饰普通方法,synchronized 关键字的锁即为 test 这个实例对象。

静态同步方法

锁是当前类的 class 对象 ,进入同步代码前要获得当前类对象的锁

Tips:对于 synchronized 作用于静态方法,锁为当前的 class,要明白与修饰普通方法的区别,普通方法的锁为创建的实例对象。为了更好地理解,我们对第 5 点讲解的代码进行微调,然后观察打印结果。

代码修改:其他代码不变,只修改如下部分代码。


  1. 新增创建一个实例对象 testNew ;
  2. 将线程 2 设置为 testNew 。
public static void main(String[] args) throws InterruptedException {
        DemoTest test = new DemoTest();
        DemoTest testNew = new DemoTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(testNew);
        t1.setName("threadOne");
        t2.setName("threadTwo");
        t1. start();
        t2. start();
    }

结果验证

threadTwo 获取到锁,其他线程在我执行完毕之前,不可进入。
threadOne 获取到锁,其他线程在我执行完毕之前,不可进入。
threadTwo: 1
threadOne: 2

结果分析:我们发现 threadTwo 和 threadOne 同时进入了该方法,为什么会出现这种问题呢?

因为我们此次的修改是新增了 testNew 这个实例对象,也就是说,threadTwo 的锁是 testNew ,threadOne 的锁是 test。

两个线程持有两个不同的锁,不会产生互相 block。相信讲到这里,同学对实例对象锁的作用也了解了,那么我们再次将 increase 方法进行修改,将其修改成静态方法,然后输出结果。

代码修改

public static synchronized void increase() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
        sleep(1000);
        count++;
        System.out.println(Thread.currentThread().getName() + ": " + count);
    }

结果验证

threadOne获取到锁,其他线程在我执行完毕之前,不可进入。
threadOne: 1
threadTwo获取到锁,其他线程在我执行完毕之前,不可进入。
threadTwo: 2

结果分析:我们看到,结果又恢复了正常,为什么会这样?

关键的原因在于,synchronized 修饰静态方法,锁为当前 class,即 DemoTest.class。

public class DemoTest extends Thread {}

无论 threadOne 和 threadTwo 如何进行 new 实例对象的创建,也不会改变锁是 DemoTest.class 的这一事实。

同步方法块

锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

Tips:对于 synchronized 作用于同步代码,锁为任何我们创建的对象,只要是个对象即可,如 new Object () 可以作为锁,new String () 也可作为锁,当然如果传入 this,那么此时代表当前对象。

我们将代码恢复到普通同步方法的知识,然后在此基础上,再次对代码进行如下修改:

代码修改:

  /**
     * synchronized 修饰实例方法
     */
    static final Object objectLock = new Object(); //创建一个对象锁
    public static void increase() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
        synchronized (objectLock) {
            sleep(1000);
            count++;
            System.out.println(Thread.currentThread().getName() + ": " + count);
        }
    }

代码解析:我们创建了一个 objectLock 作为对象锁,除了第一句打印语句,让后三句代码加入了 synchronized 同步代码块,当 threadOne 进入时,threadTwo 不可进入后三句代码的执行。

结果验证

threadOne 获取到锁,其他线程在我执行完毕之前,不可进入。
threadTwo 获取到锁,其他线程在我执行完毕之前,不可进入。
threadOne: 1
threadTwo: 2

Synchronized的底层实现原理

1. Monitor锁

synchronized的实现依赖于Java对象头中的Monitor锁。当一个线程进入同步代码块或方法时,它需要获得该对象的Monitor锁,其他线程则无法获取该锁,直到当前线程释放锁。Monitor锁的管理依赖于底层操作系统的互斥量(mutex)。

2. 对象头结构

Java对象头包含了用于存储对象自身数据和锁状态的信息。在HotSpot虚拟机中,对象头主要包含两部分:

  • Mark Word:用于存储对象的运行时数据,如哈希码、GC信息、锁状态等。
  • Class Metadata Address:指向对象的类元数据。

Mark Word在不同的锁状态下(无锁、偏向锁、轻量级锁和重量级锁)会存储不同的数据。

3. 锁的状态

synchronized锁有四种状态,随着锁竞争的加剧,锁状态会逐步升级,但升级是不可逆的:

  • 无锁状态:此时对象头中的Mark Word存储对象的哈希码。
  • 偏向锁:当一个线程第一次获得锁时,会在对象头中记录下该线程ID,如果以后该线程再次获得锁,不需要再进行CAS操作来加锁。
  • 轻量级锁:当锁被多个线程竞争时,偏向锁会升级为轻量级锁,通过CAS操作进行加锁和解锁。
  • 重量级锁:当锁竞争激烈,轻量级锁无法满足要求时,锁会膨胀为重量级锁,此时会通过操作系统的互斥量来实现线程同步。

4. 锁的升级和膨胀过程

  • 无锁状态:线程第一次进入同步块,Mark Word中存储的是对象的哈希码。
  • 偏向锁:如果只有一个线程多次进入同步块,Mark Word中记录线程ID,避免每次都进行CAS操作。
  • 轻量级锁:如果有多个线程争用锁,会尝试使用CAS操作进行锁竞争。
  • 重量级锁:如果锁竞争激烈,轻量级锁无法满足要求,会升级为重量级锁,通过操作系统的互斥量来实现同步。

5. 锁的释放

当线程退出同步块或方法时,会释放锁:

  • 偏向锁:直接释放锁,不需要CAS操作。
  • 轻量级锁:通过CAS操作释放锁。
  • 重量级锁:通过操作系统的互斥量释放锁,并唤醒等待线程。

Synchronized的性能优化

尽管synchronized关键字在Java 1.6之后得到了极大的优化,但在高并发场景下,仍可能引入较高的性能开销。以下是一些优化技巧:


  • 减少锁的粒度:尽可能缩小同步块的范围,减少持有锁的时间。
  • 读写分离:使用ReentrantReadWriteLock来区分读操作和写操作,提升并发性能。
  • 无锁数据结构:在可能的情况下,使用无锁的数据结构(如ConcurrentHashMap)来减少锁的竞争。

**示例:使用synchronized实现线程安全的计数器 **

public class SynchronizedCounter {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        SynchronizedCounter counter = new SynchronizedCounter();

        // 创建多个线程进行计数操作
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
        }

        for (Thread thread : threads) {
            thread.start();
        }

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final count: " + counter.getCount());
    }
}


总结

synchronized关键字通过Monitor锁和底层操作系统的互斥量实现线程同步,确保多线程环境下共享资源的安全访问。随着锁竞争的加剧,锁状态会从无锁逐步升级到偏向锁、轻量级锁和重量级锁。理解synchronized的底层实现原理有助于我们在高并发编程中做出更优化的设计决策。

目录
相关文章
|
3天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
46 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
10天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2天前
|
存储 物联网 大数据
探索阿里云 Flink 物化表:原理、优势与应用场景全解析
阿里云Flink的物化表是流批一体化平台中的关键特性,支持低延迟实时更新、灵活查询性能、无缝流批处理和高容错性。它广泛应用于电商、物联网和金融等领域,助力企业高效处理实时数据,提升业务决策能力。实践案例表明,物化表显著提高了交易欺诈损失率的控制和信贷审批效率,推动企业在数字化转型中取得竞争优势。
29 14
|
8天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
11天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
26 3
|
11天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
43 2
|
11天前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
58 1
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
10天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
10天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

推荐镜像

更多