Java并发——synchronized锁

在Java并发编程中,synchronized关键字是最基础也是最常用的同步手段。它能够保证在同一时刻,只有一个线程可以执行某个方法或代码块,从而解决线程安全问题。然而,很多开发者对synchronized的理解停留在"加锁"的层面,对于锁的对象是谁、不同用法有何区别等问题缺乏清晰认识。本文将从synchronized的三种应用场景入手,结合实验验证,深入剖析锁的本质,并简要探讨其底层实现原理,帮助读者彻底掌握这一关键机制。

一、并发问题与synchronized的必要性

在多线程环境中,当多个线程同时访问共享资源时,由于CPU时间片轮转和指令重排序,可能会产生数据不一致、脏读等问题。例如,对一个整型变量进行自增操作,看似简单的一行代码,在底层实际包含"读取-修改-写入"三步,若没有同步控制,就会发生线程安全问题。

synchronized通过互斥锁保证了代码块的原子性和内存可见性,是JVM内置的同步机制。它有三种主要应用形式,每种形式的锁对象各不相同,理解锁对象是正确使用synchronized的关键。

二、三种应用场景及锁对象

2.1 锁作用于实例方法

synchronized修饰一个非静态方法时,锁对象是当前实例对象 (即this)。这意味着,同一个实例的多个同步方法之间是互斥的,但不同实例的同步方法则可以并行执行。

java 复制代码
public class InstanceLockDemo {
    public synchronized void method1() {
        // 同步代码块,锁住this
        System.out.println(Thread.currentThread().getName() + "进入method1");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println(Thread.currentThread().getName() + "离开method1");
    }

    public synchronized void method2() {
        System.out.println(Thread.currentThread().getName() + "进入method2");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println(Thread.currentThread().getName() + "离开method2");
    }
}

验证实验

创建同一个实例的两个线程,一个调用method1,另一个调用method2。结果会发现,当method1持有时,method2必须等待,直至method1释放锁。反之,如果创建两个不同的实例,则两个方法可以同时执行,因为它们锁的是不同的对象。

2.2 锁作用于静态方法

synchronized修饰一个静态方法时,锁对象是当前类的Class对象。由于Class对象在JVM中只有一份,因此所有对该静态同步方法的调用,无论来自哪个实例,都会竞争同一把锁。

java 复制代码
public class StaticLockDemo {
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + "进入静态同步方法");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println(Thread.currentThread().getName() + "离开静态同步方法");
    }
}

验证实验

创建两个不同的实例,分别启动两个线程调用staticMethod。你会发现,即使实例不同,第二个线程也必须等待第一个线程执行完毕。这是因为两个线程竞争的是StaticLockDemo.class这一全局锁。

2.3 锁作用于同步代码块

同步代码块是最灵活的方式,允许我们显式指定任意对象作为锁 。锁对象可以是this、某个成员变量、甚至自定义对象。这种方式可以将锁的粒度控制得更细,减少不必要的竞争。

java 复制代码
public class BlockLockDemo {
    private final Object lock = new Object();  // 自定义锁对象
    private int count = 0;

    public void increment() {
        synchronized (lock) {   // 锁住lock对象
            count++;
        }
    }

    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }
}

验证实验

使用同一个BlockLockDemo实例,让多个线程同时调用incrementdecrement,它们会因竞争lock对象而互斥。如果将锁对象改为this,则效果与实例方法锁相同。也可以传入不同的锁对象,实现更精细的控制。

三、实验验证:代码演示锁的互斥效果

为了直观展示不同锁对象的互斥关系,下面给出一个完整的验证示例。

java 复制代码
public class SynchronizedDemo {

    // 实例方法锁:this
    public synchronized void instanceMethod() {
        System.out.println(Thread.currentThread().getName() + " 进入 instanceMethod");
        sleep(2000);
        System.out.println(Thread.currentThread().getName() + " 离开 instanceMethod");
    }

    // 静态方法锁:Class对象
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + " 进入 staticMethod");
        sleep(2000);
        System.out.println(Thread.currentThread().getName() + " 离开 staticMethod");
    }

    // 同步代码块:自定义锁对象
    private final Object customLock = new Object();
    public void customBlock() {
        synchronized (customLock) {
            System.out.println(Thread.currentThread().getName() + " 进入 customBlock");
            sleep(2000);
            System.out.println(Thread.currentThread().getName() + " 离开 customBlock");
        }
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {}
    }

    public static void main(String[] args) {
        SynchronizedDemo demo1 = new SynchronizedDemo();
        SynchronizedDemo demo2 = new SynchronizedDemo();

        // 测试1:同一实例的两个实例方法互斥
        new Thread(() -> demo1.instanceMethod(), "t1").start();
        new Thread(() -> demo1.instanceMethod(), "t2").start();

        // 测试2:不同实例的实例方法不互斥(注释后观察)
        // new Thread(() -> demo1.instanceMethod(), "t1").start();
        // new Thread(() -> demo2.instanceMethod(), "t2").start();

        // 测试3:静态方法,不同实例互斥
        // new Thread(() -> SynchronizedDemo.staticMethod(), "t3").start();
        // new Thread(() -> SynchronizedDemo.staticMethod(), "t4").start();

        // 测试4:自定义锁对象,同一实例互斥
        // new Thread(() -> demo1.customBlock(), "t5").start();
        // new Thread(() -> demo1.customBlock(), "t6").start();
    }
}

运行上述代码,可以清晰地看到锁的作用范围。建议读者动手运行,体会不同场景下的阻塞现象。

四、深入分析:synchronized的底层原理

理解锁对象之后,我们有必要了解一下synchronized在JVM层面是如何实现的。这有助于我们写出更高效、更可靠的代码。

4.1 对象头与Mark Word

每个Java对象都有一个对象头(Object Header),其中包含一个Mark Word字段,它记录了对象的哈希码、分代年龄以及锁状态信息。锁状态包括:无锁、偏向锁、轻量级锁、重量级锁。当线程获取锁时,本质上就是在修改Mark Word中的锁标志位和指向锁记录的指针。

  • 偏向锁:针对单线程重复获取同一锁的场景,通过CAS将线程ID写入Mark Word,后续无需同步操作。

  • 轻量级锁:当有竞争时,偏向锁升级为轻量级锁,通过自旋CAS尝试获取锁,避免线程阻塞。

  • 重量级锁:当竞争加剧(自旋超过一定次数),升级为重量级锁,线程进入阻塞队列,由操作系统调度。

synchronized正是通过这三级锁的状态转换,在无竞争时降低开销,在有竞争时保证正确性。

4.2 字节码层面

使用javap -c反编译包含synchronized的类文件,可以看到:

  • 同步方法:在方法上添加了ACC_SYNCHRONIZED标志,JVM通过该标志隐式获取锁。

  • 同步代码块:在字节码中插入monitorentermonitorexit指令,分别对应加锁和释放锁。

4.3 内存语义

synchronized保证了原子性和可见性。进入同步块时,线程会清空工作内存中的变量副本,重新从主内存中读取;退出同步块时,会将修改后的变量刷新到主内存。这相当于实现了lockunlock操作,保证了多线程对共享变量的可见性。

五、总结与最佳实践

  1. 明确锁对象 :实例方法锁的是this,静态方法锁的是Class对象,同步代码块锁的是任意指定对象。错误地选择锁对象(如用String常量或Integer值)可能导致意想不到的全局锁或锁失效。

  2. 控制锁粒度:同步代码块提供了最细粒度的控制,应尽量缩小同步范围,避免在锁内执行耗时操作或IO操作。

  3. 锁对象不可变 :锁对象一旦确定,不应被重新赋值,否则会导致锁失效。通常使用private final Object lock = new Object()作为专用锁。

  4. 注意死锁 :当多个锁嵌套使用时,要避免循环等待,可以通过统一顺序或使用tryLock等高级工具规避。

  5. 了解锁升级 :在JDK 1.6之后,synchronized引入了锁升级机制,性能大幅提升。但在高并发场景下,频繁的锁竞争依然会导致重量级锁的膨胀,此时可考虑使用ReentrantLockConcurrentHashMap等并发工具。

synchronized作为Java并发编程的基石,看似简单,实则蕴藏着丰富的设计智慧。通过深入理解其三种应用场景及其锁对象,并掌握底层实现原理,我们就能在开发中灵活运用,既保证线程安全,又兼顾性能。

相关推荐
☆5662 小时前
C++中的命令模式
开发语言·c++·算法
wenlonglanying2 小时前
Windows安装Rust环境(详细教程)
开发语言·windows·rust
CQU_JIAKE2 小时前
3.21【A】
开发语言·php
今儿敲了吗2 小时前
python基础学习笔记第九章——模块、包
开发语言·python
xyq20242 小时前
TypeScript 命名空间
开发语言
2301_810160952 小时前
C++与物联网开发
开发语言·c++·算法
sxlishaobin2 小时前
Java I/O 模型详解:BIO、NIO、AIO
java·开发语言·nio
cm6543202 小时前
基于C++的操作系统开发
开发语言·c++·算法
ArturiaZ2 小时前
【day57】
开发语言·c++·算法