【Java并发】synchronized锁详解

发布于:2025-02-11 ⋅ 阅读:(84) ⋅ 点赞:(0)

目录

什么是synchronized

synchronized锁用法

同步方法

1.实例方法

2.静态方法

同步代码块

1.同步实例对象

2.同步任意对象

四种用法总结

synchronized锁升级 

MarkWord 

无锁(Unlock) 

偏向锁(Biased Locking) 

轻量级锁(Lightweight Lock) 

重量级锁(Heavyweight Lock)

锁状态的升级过程

synchronized锁特点

原子性

可见性

可重入

简单易用


什么是synchronized

在Java中,synchronized关键字是一种同步机制,用于控制多个线程对共享资源的访问,以保证在任意时刻只有一个线程可以执行特定的代码段。

synchronized锁用法

同步方法

在方法声明中使用 synchronized 关键字,可以将整个方法标记为同步方法。当一个线程访问该同步方法时,其他线程将无法同时访问该对象的任何同步方法。

1.实例方法

 这种方式是将当前实例对象作为锁对象,即 this

public synchronized void instanceMethod() {
    // 方法体
}

例如:

public class Counter {
    private int count = 0;

    // 同步实例方法
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

incrementgetCount 都是同步实例方法。当多个线程同时访问 Counter 对象时,它们必须依次执行这两个方法,以确保 count 的正确性。

2.静态方法

这种方式是将当前类的 Class 对象作为锁对象。

public static synchronized void staticMethod() {
    // 方法体
}

例如:

public class Singleton {
    private static Singleton instance;

    // 同步静态方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

getInstance 是同步静态方法。当多个线程同时调用 getInstance 时,它们必须依次执行,以确保 instance 只被创建一次,实现线程安全的单例模式。

同步代码块

在代码块中使用 synchronized 关键字,可以更细粒度地控制同步范围,只对代码块中的代码进行同步。

1.同步实例对象

这种方式将当前实例对象作为锁对象。

public void someMethod() {
    synchronized (this) {
        // 只有这部分代码是同步的
    }
}

例如:

public class BankAccount {
    private double balance;

    public void deposit(double amount) {
        synchronized (this) { // 同步实例对象
            if (amount > 0) {
                balance += amount;
            }
        }
    }

    public void withdraw(double amount) {
        synchronized (this) { // 同步实例对象
            if (amount > 0 && amount <= balance) {
                balance -= amount;
            }
        }
    }
}

depositwithdraw 方法中使用同步代码块,将当前实例对象 this 作为锁对象。这样可以确保在修改 balance 时,同一时刻只有一个线程能够执行这些操作,避免并发问题。

2.同步任意对象

这种方式可以使用任意对象作为锁对象,适用于更复杂的同步控制场景。

private final Object lock = new Object();

public void anotherMethod() {
    synchronized (lock) {
        // 只有这部分代码是同步的
    }
}

例如:

public class Cache {
    private final Map<String, Object> cacheMap = new HashMap<>();
    private final Object lock = new Object();

    public void put(String key, Object value) {
        synchronized (lock) { // 同步任意对象
            cacheMap.put(key, value);
        }
    }

    public Object get(String key) {
        synchronized (lock) { // 同步任意对象
            return cacheMap.get(key);
        }
    }
}

putget 方法中使用同步代码块,将一个任意对象 lock 作为锁对象。这样可以对 cacheMap 的操作进行同步控制,确保在多个线程访问缓存时,数据的一致性和完整性。

四种用法总结

  • 同步实例方法和同步静态方法适用于简单的同步需求,但可能导致性能问题。

  • 同步代码块提供了更细粒度的同步控制,可以减少不必要的同步开销。

  • 同步任意对象的方式提供了更灵活的同步控制,适用于复杂的同步场景。

synchronized锁升级 

锁升级(Lock Coarsening)是JVM对synchronized锁进行优化的一种机制。锁升级的目的是减少锁操作的开销,提高多线程程序的性能。

在JDK 1.6之后,JVM为了提⾼锁的获取与释放效率,对synchronized的实现进⾏了优化,引⼊了偏向锁和轻量级锁,从此以后Java内置锁的状态就有了4种(无锁、偏向锁、轻量级锁和重量级锁),并且4种 状态会随着竞争的情况逐渐升级,⽽且是不可逆的过程,即不可降级,也就是说只能进⾏锁升级(从低级别到⾼级别)。

MarkWord 

Mark Word(标记字段)是Java对象头(Object Header)的一部分,它用于存储对象的运行时数据,例如哈希码、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

无锁(Unlock) 

  • 概念:无锁是指对象头中的锁标志位为01,表示对象没有被任何线程锁定。

  • 特点:当没有任何线程尝试获取对象锁时,对象处于无锁状态。此时,对象可以被任意线程获取锁。

  • 适用场景:适用于对象未被同步或者同步代码块未被任何线程执行的情况。

偏向锁(Biased Locking) 

  • 概念:偏向锁是JVM为了提高锁的获取效率而引入的一种锁状态。当一个线程第一次获取对象锁时,JVM会将对象头中的锁标志位设置为偏向锁状态,并记录获取锁的线程ID。

  • 特点

    • 偏向线程:偏向锁会偏向于第一次获取它的线程。当该线程再次获取锁时,不需要进行任何同步操作,直接成功。

    • 撤销偏向:如果其他线程尝试获取偏向锁,JVM会撤销偏向锁状态,并根据竞争情况升级为轻量级锁或重量级锁。

  • 适用场景:适用于锁被同一线程多次获取的场景,可以减少锁的获取和释放开销。

轻量级锁(Lightweight Lock) 

  • 概念:轻量级锁是当两个或多个线程交替执行同步块时,为了减少线程阻塞的开销而引入的一种锁状态。

  • 特点

    • CAS操作:轻量级锁使用CAS(Compare-And-Swap)操作来尝试获取锁。如果对象头中的锁标志位为00,表示对象处于无锁状态,线程会尝试用CAS操作将对象头中的锁标志位改为轻量级锁状态。

    • 自旋等待:当一个线程获取轻量级锁后,其他线程尝试获取锁时会进入自旋等待状态。自旋等待是一种忙等待,线程不会进入阻塞状态,而是不断尝试获取锁。

    • 锁膨胀:如果自旋等待的线程太多或者自旋时间过长,轻量级锁会膨胀为重量级锁。

  • 适用场景:适用于锁竞争不激烈且同步块执行时间较短的场景,可以减少线程阻塞的开销。

重量级锁(Heavyweight Lock)

  • 概念:重量级锁是传统的锁机制,当轻量级锁膨胀或者锁竞争激烈时,锁会升级为重量级锁。

  • 特点

    • 阻塞队列:重量级锁使用操作系统的阻塞队列来管理等待锁的线程。当一个线程获取重量级锁后,其他线程尝试获取锁时会被阻塞,进入阻塞队列等待。

    • 上下文切换:重量级锁会导致线程的上下文切换,线程从运行状态变为阻塞状态,再从阻塞状态变为运行状态需要进行上下文切换,开销较大。

  • 适用场景:适用于锁竞争激烈的场景,可以保证线程间的互斥访问。

锁状态的升级过程

  • 无锁 -> 偏向锁:当一个线程第一次获取对象锁时,如果没有其他线程竞争,锁会从无锁状态升级为偏向锁状态。

  • 偏向锁 -> 轻量级锁:当其他线程尝试获取偏向锁时,偏向锁状态会被撤销,并升级为轻量级锁状态。

  • 轻量级锁 -> 重量级锁:当轻量级锁的自旋等待线程太多或者自旋时间过长时,锁会膨胀为重量级锁状态。

注意:锁状态的升级是不可逆的:锁状态只能从无锁升级到偏向锁、轻量级锁、重量级锁,不能从重量级锁降级到轻量级锁或偏向锁。

           锁优化的目的是提高性能:JVM通过锁状态的升级和优化,减少了锁的获取和释放开销,提高了多线程程序的性能。开发者在编写代码时,不需要显式地管理锁状态,JVM会根据实际情况自动进行优化。

synchronized锁特点

原子性

当一个线程进入被 synchronized 修饰的代码块时,其他线程无法同时进入该代码块。这确保了代码块内的操作是原子性的,即要么全部执行,要么全部不执行,不会出现部分执行的情况。

可见性

当一个线程修改了被 synchronized 修饰的共享变量后,其他线程能够立即看到这个修改。这是因为 synchronized 会将共享变量的修改强制刷新到主内存中,其他线程在获取锁时会从主内存中读取最新的值。 

可重入

synchronized 会维护一个锁持有计数来追踪同一个线程对锁的多次获取。每次线程获取锁时,计数加一;每次释放锁时,计数减一。只有当计数归零时,其他线程才能获取该锁。

简单易用

synchronized 的使用语法简单明了,可以通过同步方法或同步代码块的方式轻松实现线程同步。