基本使用

Java中的synchronized关键字用于在多线程环境下确保数据同步。它可以用来修饰方法和代码块

当一个线程访问一个对象的synchronized方法或代码块时,其他线程将无法访问该对象的其他synchronized方法或代码块。这样可以确保在同一时间只有一个线程能够执行该代码块或方法,避免了多线程环境下的数据不一致问题

例如:

public class SynchronizedExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}

在上面的代码中,increment()方法是一个synchronized方法。当多个线程访问这个方法时,只有一个线程能够执行该方法的代码,其他线程将被阻塞

synchronized关键字也可以用来修饰代码块,如:

public void increment() {
    synchronized(this) {
        count++;
    }
}

在上面的代码中,synchronized关键字修饰的是一个代码块,并且锁对象是当前对象(this)

注意:synchronized关键字会导致线程上下文切换和资源竞争,所以在使用时要注意性能问题

源码解析

底层实现是通过 Java 虚拟机(JVM)的对象头和监视器锁机制实现的

具体来说,当一个线程访问一个对象的 synchronized 方法或代码块时,它会试图获取该对象的监视器锁。如果该锁未被其他线程占用,该线程将获得该锁并执行代码;如果该锁被其他线程占用,该线程将进入阻塞状态,等待获取该锁

这里给出一份简化的 synchronized 关键字的源码:

public void synchronized method() {
    // 加锁
    Monitor.enter(this);
    try {
        // 同步代码块
    } finally {
        // 释放锁
        Monitor.exit(this);
    }
}

在这份代码中,方法通过调用 Monitor.enter 方法获取当前对象的监视器锁,并在 finally 块中调用 Monitor.exit 方法释放该锁。因此,在 synchronized 方法内部的代码可以保证在任意时刻只有一个线程可以访问

底层原理

Synchronized 关键字的底层源码是由 Java 编译器在编译期间生成的

当使用 synchronized 关键字修饰方法时,编译器会在该方法的字节码中插入 monitor 指令,用于管理该方法的访问顺序

当使用 synchronized 关键字修饰代码块时,编译器会生成两个字节码指令:monitorenter 和 monitorexit,用于在该代码块的开始和结束处加锁和解锁

以下是 synchronized 关键字修饰方法的示例汇编代码:

0: aload_0        # 加载 this 引用
1: dup            # 复制 this 引用
2: astore_1       # 将复制的 this 引用存入局部变量 1
3: monitorenter   # 对局部变量 1 上的对象加锁
4: ...            # 同步方法体
5: aload_1        # 重新加载 this 引用
6: monitorexit    # 对局部变量 1 上的对象解锁
7: ...

以下是 synchronized 关键字修饰代码块的示例汇编代码:

0: aload_1        # 加载 synchronized 关键字后的对象引用
1: dup            # 复制对象引用
2: astore_2       # 将复制的对象引用存入局部变量 2
3: monitorenter   # 对局部变量 2 上的对象加锁
4: ...            # 同步代码块体
5: aload_2        # 重新加载对象引用
6: monitorexit    # 对局部变量 2 上的

使用案例代码

public class Bank {
    private int balance = 100;

    public synchronized int getBalance() {
        return balance;
    }

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public synchronized void withdraw(int amount) {
        balance -= amount;
    }
}

public class User extends Thread {
    private Bank bank;
    private int amount;

    public User(Bank bank, int amount) {
        this.bank = bank;
        this.amount = amount;
    }

    public void run() {
        bank.withdraw(amount);
        System.out.println("User completed withdrawal, balance: " + bank.getBalance());
    }
}

public class Main {
    public static void main(String[] args) {
        Bank bank = new Bank();
        for (int i = 0; i < 100; i++) {
            User user = new User(bank, 10);
            user.start();
        }
    }
}

在这个例子中,Bank 类表示一个银行账户,提供存款、取款和查询余额等功能。User 类表示一个用户,继承自 Thread 类,并在启动后进行取款操作

为了保证在多线程并发访问时不会出现错误,我们使用了 synchronized 关键字修饰了 Bank 类的 getBalance、deposit 和 withdraw 方法,确保在任意时刻只有一个线程可以访问这些方法

常见面试题

synchronized 方法和 synchronized 块的区别是什么?

  1. 作用范围:synchronized 方法将整个方法体作为同步区块,而 synchronized 块可以将任意代码块作为同步区块。
  2. 锁的对象:synchronized 方法锁定的是整个对象,而 synchronized 块锁定的是在括号内指定的对象。
  3. 可控性:synchronized 方法的同步粒度比较大,不够灵活;而 synchronized 块可以更灵活地控制同步代码块的大小。

综上所述,在确定同步粒度时,通常使用 synchronized 块比使用 synchronized 方法更灵活,但是如果整个方法都需要同步,使用 synchronized 方法会更加简单易懂

什么情况下可以使用 synchronized 关键字?

synchronized 关键字可以用于在多线程环境下保证方法或代码块的原子性。具体来说,如果一个线程正在执行同步方法或代码块,则其他线程将无法访问该方法或代码块
常见情况包括:

  • 当多个线程访问共享资源时,可以使用 synchronized 关键字保证线程的安全
  • 在访问共享变量时,需要对其进行同步控制
  • 在线程通信中,可以使用 synchronized 关键字保证线程之间的同步通信

synchronized 关键字的性能开销如何?

synchronized 关键字的使用会带来一些性能开销,因为它需要在多个线程之间进行同步。当线程访问同步代码块时,它必须获得锁,这会增加额外的开销。如果同步代码块执行时间过长,其他线程将一直等待,进而降低程序的性能

因此,应该尽量避免在高并发情况下使用 synchronized,或者使用其他的并发控制机制,如 java.util.concurrent 包中的锁和原子操作类等

synchronized 关键字如何实现可重入?

“可重入” 指的是同一线程可以多次获取同一个锁。例如,当线程 A 进入一个同步块时,如果它再次试图进入该块,则可以再次获取锁,而不会发生死锁

在 Java 中,synchronized 关键字可以实现可重入,原因如下:

  • synchronized 关键字使用对象监视器锁来实现同步
  • 对象监视器锁是基于线程的,并且每个线程有一个独立的计数器,用于跟踪它在当前对象上获取的锁的数量
  • 当线程试图获取锁时,如果它已经拥有该锁,则计数器将递增
  • 当线程退出同步块时,计数器将递减
  • 只有当计数器为零时,该线程才会释放锁

因此,如果一个线程在同一对象上多次进入同步块,它将多次获得该锁,并在退出该块时多次释放该锁。因此,synchronized 关键字是可重入的

synchronized 关键字与 lock 机制的比较?

synchronized 关键字和 Lock 机制都是用来保证线程同步的方法。但是它们有一些明显的差异:

  1. 灵活性:Lock 机制比 synchronized 关键字更灵活,因为它提供了更多的锁定操作,例如可以实现公平锁和非公平锁,还可以实现读写锁
  2. 可中断性:Lock 机制可以中断一个线程的等待,而 synchronized 关键字不能
  3. 可重入性:synchronized 关键字是自动可重入的,而 Lock 机制必须手动实现
  4. 性能:如果比较的是相同的锁定操作,synchronized 关键字通常比 Lock 机制更快,因为它是内置的

总体而言,在简单的同步情况下,synchronized 关键字更方便,但是在需要更多灵活性的情况下,Lock 机制可能是一个更好的选择

转自:https://blog.csdn.net/qq_37712731/article/details/128864311