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

简介: 深入解析 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的底层实现原理有助于我们在高并发编程中做出更优化的设计决策。

目录
相关文章
|
2天前
|
Java 开发者 C++
Java多线程同步大揭秘:synchronized与Lock的终极对决!
【6月更文挑战第20天】在Java多线程编程中,`synchronized`和`Lock`是两种关键的同步机制。`synchronized`作为内置关键字提供基础同步,简单但可能不够灵活;而`Lock`接口自Java 5引入,提供更复杂的控制和优化性能的选项。在低竞争场景下,`synchronized`性能可能更好,但在高并发或需要精细控制时,`Lock`(如`ReentrantLock`)更具优势。选择哪种取决于具体需求和场景,理解两者机制至关重要。
|
2天前
|
安全 Java
不懂synchronized?那你可能错过了Java的“半壁江山”!
【6月更文挑战第20天】Java并发核心:`synchronized`关键字确保线程同步,防止数据不一致。通过监视器锁实现,修饰方法或代码块,保证原子性。示例展示了如何使用`synchronized`维持计数器的线程安全。此外,它能细粒度控制锁,与`volatile`区别在于`synchronized`提供阻塞式同步,确保原子性操作。掌握它,是征服Java多线程的关键。
|
2天前
|
Java 测试技术
Java多线程同步实战:从synchronized到Lock的进化之路!
【6月更文挑战第20天】Java多线程同步始于`synchronized`关键字,保证单线程访问共享资源,但为应对复杂场景,`Lock`接口(如`ReentrantLock`)提供了更细粒度控制,包括可重入、公平性及中断等待。通过实战比较两者在高并发下的性能,了解其应用场景。不断学习如`Semaphore`等工具并实践,能提升多线程编程能力。从同步起点到专家之路,每次实战都是进步的阶梯。
|
2天前
|
Java 数据安全/隐私保护
深入剖析:Java Socket编程原理及客户端-服务器通信机制
【6月更文挑战第21天】Java Socket编程用于构建网络通信,如在线聊天室。服务器通过`ServerSocket`监听,接收客户端`Socket`连接请求。客户端使用`Socket`连接服务器,双方通过`PrintWriter`和`BufferedReader`交换数据。案例展示了服务器如何处理每个新连接并广播消息,以及客户端如何发送和接收消息。此基础为理解更复杂的网络应用奠定了基础。
|
1天前
|
Java 机器人 数据库连接
Java中的内存泄漏问题解析与应对
Java中的内存泄漏问题解析与应对
|
2天前
|
存储 安全 算法
Java并发编程中的线程安全性与性能优化
在Java编程中,特别是涉及并发操作时,线程安全性及其与性能优化是至关重要的问题。本文将深入探讨Java中线程安全的概念及其实现方式,以及如何通过性能优化策略提升程序的并发执行效率。
8 1
|
2天前
|
前端开发 Java
java加载class文件的原理
java加载class文件的原理
|
3天前
|
安全 Java 程序员
惊呆了!Java多线程里的“synchronized”竟然这么神奇!
【6月更文挑战第20天】Java的`synchronized`关键字是解决线程安全的关键,它确保同一时间只有一个线程访问同步代码。在案例中,`Counter`类的`increment`方法如果不加同步,可能会导致竞态条件。通过使用`synchronized`方法或语句块,可以防止这种情况,确保线程安全。虽然同步会带来性能影响,但它是构建并发应用的重要工具,平衡同步与性能是使用时需考虑的。了解并恰当使用`synchronized`,能有效应对多线程挑战。
|
3天前
|
Java
JAVA多线程深度解析:线程的创建之路,你准备好了吗?
【6月更文挑战第19天】Java多线程编程提升效率,通过继承Thread或实现Runnable接口创建线程。Thread类直接继承启动简单,但限制多继承;Runnable接口实现更灵活,允许类继承其他类。示例代码展示了两种创建线程的方法。面对挑战,掌握多线程,让程序高效运行。
|
16小时前
|
Java Android开发 Kotlin
Android面试题:App性能优化之Java和Kotlin常见的数据结构
Java数据结构摘要:ArrayList基于数组,适合查找和修改;LinkedList适合插入删除;HashMap1.8后用数组+链表/红黑树,初始化时预估容量可避免扩容。SparseArray优化查找,ArrayMap减少冲突。 Kotlin优化摘要:Kotlin的List用`listOf/mutableListOf`,Map用`mapOf/mutableMapOf`,支持操作符重载和扩展函数。序列提供懒加载,解构用于遍历Map,扩展函数默认参数增强灵活性。
8 0

推荐镜像

更多