杰瑞科技汇

java 方法synchronized

synchronized 是 Java 中用于解决多线程并发问题最基本、最常用的工具,它是一种内置锁(也称为监视器锁互斥锁),能够确保在同一时刻,只有一个线程可以执行被 synchronized 修饰的代码块或方法,从而保证了线程安全。

java 方法synchronized-图1
(图片来源网络,侵删)

synchronized 的核心作用

  1. 原子性:确保一个代码块或方法的操作是不可分割的,一个线程一旦开始执行,就必须执行完毕,中间不会被其他线程打断。
  2. 可见性:当一个线程释放锁时,它在该临界区内对共享变量的所有修改都会被刷新到主内存中,当另一个线程获取同一个锁时,它会从主内存中读取这些共享变量,从而保证了线程间的可见性。
  3. 有序性synchronized 会禁止指令重排序,保证了代码的执行顺序与代码的编写顺序一致。

synchronized 的三种使用方式

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

synchronized 实例方法

synchronized 修饰一个非静态(实例)方法时,锁是当前对象实例this)。

语法:

public synchronized void instanceMethod() {
    // 临界区代码
}

工作原理:

java 方法synchronized-图2
(图片来源网络,侵删)
  • 假设有两个线程 Thread-AThread-B,它们都调用了同一个对象 myObjectinstanceMethod() 方法。
  • Thread-A 调用时,它会获取 myObject 这个对象的锁。
  • Thread-A 执行完方法并释放锁之前,Thread-B 如果也想调用 myObjectinstanceMethod(),它必须等待,因为 myObject 的锁已经被 Thread-A 占用了。
  • 注意Thread-B 调用的是 myObject2(另一个不同的对象实例)的 instanceMethod(),那么它不需要等待,因为它们锁的是不同的对象。

示例:

class Counter {
    private int count = 0;
    // 锁是 this 对象
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + counter.getCount()); // 输出应为 2000
    }
}

synchronized 静态方法

synchronized 修饰一个静态方法时,锁是当前类的 Class 对象MyClass.class)。

语法:

public static synchronized void staticMethod() {
    // 临界区代码
}

工作原理:

java 方法synchronized-图3
(图片来源网络,侵删)
  • 这个锁属于类级别,而不是对象级别。
  • 无论有多少个该类的对象实例,对于这个静态方法的锁都是同一个——Class 对象。
  • 当一个线程访问 obj1.staticMethod() 时,另一个线程访问 obj2.staticMethod()obj1obj2 是同一个类的不同实例)时,它们会互相等待,因为它们竞争的是同一个 Class 对象的锁。

示例:

class SharedResource {
    // 锁是 SharedResource.class 对象
    public static synchronized void staticOperation() {
        System.out.println(Thread.currentThread().getName() + " is in staticOperation.");
        try {
            Thread.sleep(2000); // 模拟耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " is leaving staticOperation.");
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> SharedResource.staticOperation(), "Thread-1");
        Thread t2 = new Thread(() -> SharedResource.staticOperation(), "Thread-2");
        t1.start();
        t2.start();
    }
}

预期输出Thread-1 会先进入,执行2秒后退出,Thread-2 才能进入,它们不会并行执行。

synchronized 代码块

这是最灵活、最常用的一种方式,它可以指定锁对象,从而只锁定代码中真正需要同步的部分,而不是整个方法,提高了并发度。

语法:

// 锁是任意对象
synchronized (lockObject) {
    // 临界区代码
}

工作原理:

  • lockObject 可以是任何对象,但必须是同一个对象才能起到互斥作用。
  • 我们会使用一个专门用于加锁的 private final Object,以避免外部代码意外修改锁对象。
  • 如果锁对象是 this,那么效果等同于 synchronized 实例方法。
  • 如果锁对象是 ClassName.class,那么效果等同于 synchronized 静态方法。

示例:

class CounterWithBlock {
    private int count = 0;
    // 创建一个专用的锁对象
    private final Object lock = new Object();
    public void increment() {
        // 只对需要同步的代码块加锁
        synchronized (lock) {
            count++;
        }
        // 这里的代码不需要同步,可以和其他线程并行执行
        // ... some other non-critical code ...
    }
    // 等价于 synchronized (this) 的写法
    public void anotherIncrement() {
        synchronized (this) {
            count++;
        }
    }
    public int getCount() {
        return count;
    }
}

优点

  • 粒度更细:只锁定必要的代码,减少锁的持有时间,提高性能。
  • 灵活性高:可以选择任意对象作为锁,实现更复杂的同步逻辑。

synchronized 的底层原理

synchronized 的底层实现依赖于 JVM 的 Monitor(监视器)机制。

  1. 对象头:在 Java 中,每个对象在内存中都有对象头,对象头里存储了对象的运行时数据(如哈希码、GC分代年龄)和类型指针,更重要的是,它还包含一个指向Monitor的指针。

  2. Monitor:可以理解为一个同步工具或一个同步对象,每个对象都可以关联一个 Monitor。

    • 当一个线程想要进入被 synchronized 保护的代码块时,它必须先获取与对象关联的 Monitor 的所有权。
    • Monitor 内部有一个计数器(_count)和一个等待队列(_WaitSet)。
    • 获取锁_count 为 0,表示 Monitor 没有被占用,线程将 _count 设为 1,成功获取锁。_count 不为 0,说明已经有线程持有了锁,该线程就会被放入 _WaitSet 中进行阻塞等待。
    • 释放锁:持有锁的线程执行完毕后,会将 _count 减 1,当 _count 减为 0 时,表示锁被释放,JVM 会从 _WaitSet 中唤醒一个线程去尝试获取锁。
  3. 锁升级(JDK 1.6 优化): 为了提升性能,JVM 对 synchronized 做了大量优化,引入了锁升级的概念:

    • 偏向锁:假设一个锁总是由同一个线程获取,当一个线程获取锁时,JVM 会将锁的“偏向”设置给该线程,之后该线程再获取锁时,就不需要进行 CAS(Compare-And-Swap)操作了,开销极小。
    • 轻量级锁:当有另一个线程竞争锁时,偏向锁会升级为轻量级锁,竞争线程会通过自旋(忙等待)的方式尝试获取锁,而不是立即阻塞,自旋可以避免线程切换的开销。
    • 重量级锁:如果自旋一定次数后仍然没有获取到锁,或者有更多线程竞争,轻量级锁就会升级为重量级锁,未获取到锁的线程会被阻塞,并进入操作系统内核态的等待队列,等待操作系统来唤醒。

这个锁升级的过程是自动的,旨在让 synchronized 在不同场景下都能有较好的性能。


synchronized 的优缺点

优点:

  1. 使用简单:语法直观,易于理解和实现。
  2. 非阻塞同步:在竞争不激烈的情况下,经过优化的 synchronized(偏向锁、轻量级锁)性能非常高,接近于无锁。
  3. 保证可见性:J
分享:
扫描分享到社交APP
上一篇
下一篇