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

本文涉及的产品
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 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的底层实现原理有助于我们在高并发编程中做出更优化的设计决策。

目录
相关文章
|
1天前
|
安全 Java 开发者
Java并发编程中的线程安全性与性能优化
在Java编程中,处理并发问题是至关重要的。本文探讨了Java中线程安全性的概念及其在性能优化中的重要性。通过深入分析多线程环境下的共享资源访问问题,结合常见的并发控制手段和性能优化技巧,帮助开发者更好地理解和应对Java程序中的并发挑战。 【7月更文挑战第14天】
|
5天前
|
缓存 监控 算法
Java面试题:描述Java垃圾回收的基本原理,以及如何通过代码优化来协助垃圾回收器的工作
Java面试题:描述Java垃圾回收的基本原理,以及如何通过代码优化来协助垃圾回收器的工作
28 8
|
1天前
|
监控 Java API
Java并发编程之线程池深度解析
【7月更文挑战第14天】在Java并发编程领域,线程池是提升性能、管理资源的关键工具。本文将深入探讨线程池的核心概念、内部工作原理以及如何有效使用线程池来处理并发任务,旨在为读者提供一套完整的线程池使用和优化策略。
|
3天前
|
存储 监控 Java
揭秘Java虚拟机:探索JVM的工作原理与性能优化
本文深入探讨了Java虚拟机(JVM)的核心机制,从类加载到垃圾回收,再到即时编译技术,揭示了这些复杂过程如何共同作用于Java程序的性能表现。通过分析现代JVM的内存管理策略和性能监控工具,文章提供了实用的调优建议,帮助开发者有效提升Java应用的性能。
18 3
|
4天前
|
存储 安全 算法
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第72天】 在现代软件开发中,尤其是Java应用开发领域,并发编程是一个无法回避的重要话题。随着多核处理器的普及,合理利用并发机制对于提高软件性能、响应速度和资源利用率具有重要意义。本文旨在探讨Java并发编程的核心概念、线程安全的策略以及性能优化技巧,帮助开发者构建高效且可靠的并发应用。通过实例分析和理论阐述,我们将揭示在高并发环境下如何平衡线程安全与系统性能之间的关系,并提出一系列最佳实践方法。
|
5天前
|
算法 Java
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
13 1
|
1天前
|
Java 开发者
Java并发编程中的锁机制与性能优化
【7月更文挑战第14天】本文深入探讨了Java中锁的概念、种类及其在并发编程中的应用,并分析了不同锁类型对程序性能的影响。通过实例展示了如何合理选择和使用锁来提升应用的性能,同时指出了锁使用过程中可能遇到的问题和调优策略。旨在为Java开发者提供锁机制的深入理解和性能优化的实用建议。
|
5天前
|
缓存 Java
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
8 0
|
5天前
|
存储 安全 Java
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
9 0
|
5天前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
9 0

推荐镜像

更多