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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入解析 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实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
3天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
4天前
|
存储 分布式计算 Java
存算分离与计算向数据移动:深度解析与Java实现
【11月更文挑战第10天】随着大数据时代的到来,数据量的激增给传统的数据处理架构带来了巨大的挑战。传统的“存算一体”架构,即计算资源与存储资源紧密耦合,在处理海量数据时逐渐显露出其局限性。为了应对这些挑战,存算分离(Disaggregated Storage and Compute Architecture)和计算向数据移动(Compute Moves to Data)两种架构应运而生,成为大数据处理领域的热门技术。
18 2
|
4天前
|
设计模式 安全 Java
Java编程中的单例模式深入解析
【10月更文挑战第31天】在编程世界中,设计模式就像是建筑中的蓝图,它们定义了解决常见问题的最佳实践。本文将通过浅显易懂的语言带你深入了解Java中广泛应用的单例模式,并展示如何实现它。
|
4天前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
6 0
|
28天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
63 0
|
28天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
50 0
|
28天前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
58 0
|
28天前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
74 0

推荐镜像

更多
下一篇
无影云桌面