杰瑞科技汇

synchronized如何保证线程安全与性能优化?

目录

  1. synchronized 是什么?
  2. synchronized 的作用
  3. synchronized 的三种使用方式
    • 修饰实例方法
    • 修饰静态方法
    • 修饰代码块
  4. synchronized 的底层原理
    • Java 对头
    • Monitor 监视器
    • 锁升级机制(偏向锁、轻量级锁、重量级锁)
  5. synchronized 的特性
    • 原子性
    • 可见性
    • 有序性
  6. synchronizedReentrantLock 的对比
  7. 最佳实践与注意事项

synchronized 是什么?

synchronized 是 Java 中的一个关键字,中文意思是“同步的”,它是一种内置锁(或称为监视器锁,Monitor Lock),主要用于确保在同一时间,只有一个线程可以执行被 synchronized 保护的代码块或方法,通过这种方式,它可以有效防止多线程并发执行时导致的数据不一致、脏读等问题。

synchronized如何保证线程安全与性能优化?-图1
(图片来源网络,侵删)

可以把它想象成一个房间的钥匙,当一个线程进入被 synchronized 保护的“房间”(代码块)时,它会拿起钥匙并锁上门,其他想要进入这个房间的线程,只能在门外等待,直到里面的线程执行完毕,走出房间,并把钥匙放回原处。

synchronized 的作用

synchronized 主要有以下三个核心作用:

  1. 原子性:确保一个代码块或方法在同一时间只被一个线程执行,一个线程在执行完临界区代码之前,其他线程无法进入。
  2. 可见性:当一个线程释放锁时,JMM(Java 内存模型)会强制将该线程工作内存中的所有共享变量的最新值刷新到主内存中,当另一个线程在获取同一个锁时,JMM 会强制将该线程的工作内存置为无效,从而需要从主内存中重新读取共享变量的值,这样就保证了线程间的可见性。
  3. 有序性synchronized 关键字会禁止指令重排序优化,从而保证了代码块的执行顺序。

synchronized 的三种使用方式

synchronized 可以用在三个地方:实例方法、静态方法和代码块。

修饰实例方法

synchronized 修饰一个非静态的实例方法时,它锁定的对象是当前实例对象 this

synchronized如何保证线程安全与性能优化?-图2
(图片来源网络,侵删)
public class SynchronizedMethodExample {
    // 锁的是 this 对象
    public synchronized void instanceMethod() {
        System.out.println(Thread.currentThread().getName() + " 开始执行实例方法");
        try {
            // 模拟耗时操作
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完实例方法");
    }
}

特点

  • 锁是对象锁,锁的是当前对象。
  • 同一个类的不同实例对象,它们的锁是互不干扰的,线程 A 调用 obj1.instanceMethod() 不会阻塞线程 B 调用 obj2.instanceMethod()

修饰静态方法

synchronized 修饰一个静态方法时,它锁定的对象是当前类的 Class 对象(ClassName.class

public class SynchronizedStaticMethodExample {
    // 锁的是 SynchronizedStaticMethodExample.class 对象
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + " 开始执行静态方法");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完静态方法");
    }
}

特点

  • 锁是类锁,锁的是 Class 对象。
  • 对于一个类的所有实例,它们共享同一个 Class 对象,线程 A 调用 obj1.staticMethod() 会阻塞线程 B 调用 obj2.staticMethod()

修饰代码块

这是最灵活的方式,可以指定锁定的对象。

synchronized如何保证线程安全与性能优化?-图3
(图片来源网络,侵删)
public class SynchronizedBlockExample {
    private final Object lock = new Object(); // 一个专门用于加锁的对象
    public void blockMethod1() {
        // 锁定 this 对象
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " 开始执行代码块1");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 执行完代码块1");
        }
    }
    public void blockMethod2() {
        // 锁定一个自定义的 lock 对象
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 开始执行代码块2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 执行完代码块2");
        }
    }
    public static void staticBlockMethod() {
        // 锁定当前类的 Class 对象
        synchronized (SynchronizedBlockExample.class) {
            System.out.println(Thread.currentThread().getName() + " 开始执行静态代码块");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 执行完静态代码块");
        }
    }
}

特点

  • 灵活性最高:可以精确地控制锁的范围,避免锁定整个方法,从而减少锁的竞争,提高并发性能。
  • 锁定任意对象:可以锁定 thisclass 对象,也可以是一个自定义的、专门用于加锁的 Object 实例。
  • 最佳实践不要用字符串常量或包装类(如 Integer)作为锁对象,因为在 JVM 中,字符串常量和特定范围内的包装类可能会被 JVM 进行缓存和重用,导致意想不到的锁竞争。

synchronized 的底层原理

synchronized 的实现依赖于 JVM 和操作系统,其核心是 Monitor(监视器)

Java 对头

在 JVM 中,每个 Java 对象都可以关联一个 Monitor,这个 Monitor 信息存储在对象的对象头中,对象头主要包括两部分信息:

  • Mark Word:存储对象的哈希码、分代年龄、锁标志位等。
  • 类型指针:指向对象的类元数据。

Monitor 监视器

Monitor 可以理解为一个同步工具,它被线程持有后,其他线程就无法再进入被该 Monitor 保护的代码区域,Monitor 的主要数据结构如下:

  • owner:指向持有锁的线程。
  • EntryList:存放等待获取锁的线程的队列。
  • WaitSet:存放调用 wait() 方法后进入等待状态的线程的队列。

执行流程

  1. 当线程尝试进入 synchronized 代码块时,它会尝试获取对象的 Monitor。
  2. Monitor 的 owner 为空,则当前线程成功获取锁,并将 owner 设置为自己。
  3. owner 不为空(已被其他线程持有),则当前线程进入 EntryList 中阻塞,并挂起。
  4. 当持有锁的线程执行完 synchronized 代码块后,它会释放锁(将 owner 置为 null),并唤醒 EntryList 中的一个线程,使其去竞争锁。
  5. 如果线程在 synchronized 代码块中调用了 wait(),它会释放锁,并进入 WaitSet 中等待。

锁升级机制(重要!)

为了提高性能,HotSpot JVM 对 synchronized 的锁进行了优化,引入了锁升级的概念,从低到高依次为:

  1. 偏向锁

    • 假设:锁总是由同一个线程获取。
    • 过程:当一个线程第一次获取锁时,JVM 会将对象的 Mark Word 中的锁标志位设置为“偏向模式”,并记录获取锁的线程 ID,之后,这个线程每次进入同步块时,JVM 都不需要再进行任何同步操作,只需检查 Mark Word 中的线程 ID 是否是当前线程即可。
    • 优点:极大地减少了没有竞争情况下的同步开销。
    • 消除:当有另一个线程竞争该锁时,偏向锁会立即升级为轻量级锁。
  2. 轻量级锁

    • 假设:锁的竞争不激烈,线程交替获取锁。
    • 过程:当有竞争时,JVM 会将对象的 Mark Word 复制到当前线程的栈帧中,并建立一个名为锁记录的空间,然后尝试使用 CAS(Compare-And-S
分享:
扫描分享到社交APP
上一篇
下一篇