杰瑞科技汇

synchronized锁对象和锁方法有何区别?

Java synchronized用法详解:从原理到实战,一篇搞定线程安全

** 还在为Java线程安全头疼?本文带你彻底搞懂synchronized关键字的用法、原理及最佳实践,让你的代码告别并发Bug!

synchronized锁对象和锁方法有何区别?-图1
(图片来源网络,侵删)

引言:为什么Java并发编程离不开synchronized?

在Java的世界里,多线程就像一把双刃剑,它能充分利用CPU资源,提升程序性能,但也带来了无数令人头疼的线程安全问题,当多个线程同时共享和修改同一个数据时,如果不加以控制,就可能导致数据不一致、程序崩溃等严重后果。

synchronized,作为Java并发编程的元老级关键字,为我们提供了一种简单、可靠的机制来保证代码块的原子性、可见性和有序性,从而解决线程安全问题,无论是初学者还是资深开发者,深入理解synchronized的用法都是一项必备技能。

本文将带你从零开始,全面、系统地剖析Java synchronized的用法,并结合实例代码,让你彻底掌握它。


synchronized是什么?核心作用揭秘

synchronized是Java中的一个关键字,中文意为“同步”,它可以用来修饰以下几种结构:

synchronized锁对象和锁方法有何区别?-图2
(图片来源网络,侵删)
  1. 实例方法
  2. 静态方法
  3. 代码块

其核心作用是确保在同一时刻,只有一个线程可以执行被synchronized修饰的代码,这就像给一段代码加了一把“锁”,任何想要执行这段代码的线程,都必须先获取到这把锁。


synchronized的三种用法详解(附代码示例)

synchronized的用法灵活多样,我们分别来看它在不同场景下的应用。

修饰实例方法(对象锁)

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

代码示例:

synchronized锁对象和锁方法有何区别?-图3
(图片来源网络,侵删)
public class SynchronizedMethodExample {
    private int count = 0;
    // synchronized修饰实例方法
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " - Count: " + count);
    }
    public int getCount() {
        return count;
    }
    public static void main(String[] args) {
        SynchronizedMethodExample example = new SynchronizedMethodExample();
        // 创建两个线程,同时调用increment方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        }, "Thread-A");
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        }, "Thread-B");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final Count: " + example.getCount()); // 输出应为10
    }
}

原理解析:

在上面的例子中,thread1thread2共享同一个example对象,当thread1调用increment()方法时,它会获取example对象的锁,在thread1执行完increment()方法并释放锁之前,thread2无法进入该方法,这就保证了count++操作的原子性,最终结果正确。

关键点:

  • 锁的是对象本身(this)
  • 不同对象实例的synchronized方法互不干扰,因为它们锁的是不同的对象。

修饰静态方法(类锁)

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

代码示例:

public class SynchronizedStaticMethodExample {
    private static int staticCount = 0;
    // synchronized修饰静态方法
    public static synchronized void staticIncrement() {
        staticCount++;
        System.out.println(Thread.currentThread().getName() + " - Static Count: " + staticCount);
    }
    public static int getStaticCount() {
        return staticCount;
    }
    public static void main(String[] args) {
        // 创建两个线程,调用静态方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                SynchronizedStaticMethodExample.staticIncrement();
            }
        }, "Thread-C");
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                SynchronizedStaticMethodExample.staticIncrement();
            }
        }, "Thread-D");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final Static Count: " + SynchronizedStaticMethodExample.getStaticCount()); // 输出应为10
    }
}

原理解析:

无论创建多少个SynchronizedStaticMethodExample的实例,它们的staticIncrement()方法都共享同一个Class对象,当一个线程(如thread1)调用staticIncrement()时,它会锁定SynchronizedStaticMethodExample.class,其他所有线程(包括其他实例的线程)都无法再调用该类的任何synchronized静态方法,直到锁被释放。

关键点:

  • 锁的是类的Class对象
  • 所有实例共享这把“类锁”,不同实例的synchronized静态方法会相互排斥。

修饰代码块(更灵活的锁)

这是synchronized最灵活、也是最推荐的用法之一,它可以指定任意对象作为锁,从而精确控制锁的粒度。

语法: synchronized (锁对象) { // 需要同步的代码 }

代码示例:

public class SynchronizedBlockExample {
    private int count = 0;
    // 定义一个锁对象
    private final Object lock = new Object();
    public void incrementWithBlock() {
        // synchronized代码块,锁定lock对象
        synchronized (lock) {
            count++;
            System.out.println(Thread.currentThread().getName() + " - Block Count: " + count);
        }
    }
    public int getCount() {
        return count;
    }
    public static void main(String[] args) {
        SynchronizedBlockExample example = new SynchronizedBlockExample();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.incrementWithBlock();
            }
        }, "Thread-E");
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.incrementWithBlock();
            }
        }, "Thread-F");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final Block Count: " + example.getCount()); // 输出应为10
    }
}

原理解析:

在这个例子中,我们创建了一个普通的Object实例lock作为锁,只有当线程进入synchronized (lock)代码块时,才会尝试获取lock对象的锁,这种方式的优点是:

  1. 锁粒度更细:可以只对关键代码段加锁,而不是整个方法,减少锁的持有时间,提高并发性能。
  2. 锁对象可定制:可以灵活选择锁定哪个对象,例如锁定方法参数、类的私有成员等。

关键点:

  • 锁的是括号中指定的对象
  • 必须保证多个线程使用的是同一个锁对象,否则锁机制失效。
  • 通常推荐使用private final Object作为锁对象,避免外部误修改。

synchronized的底层原理:JVM是如何实现锁的?

了解底层原理能帮助我们更深刻地理解synchronized的行为,在Java 6之后,JVM对synchronized进行了大量优化,其实现主要涉及三种锁状态:

  1. 偏向锁(Biased Locking)

    • 思想:假设锁总是由同一个线程获取,当一个线程第一次获取锁时,JVM会将这个线程的ID记录在锁对象头中,该线程之后再次获取锁时,无需进行CAS(Compare-And-Swap)操作,直接进入,开销极小。
    • 场景:适用于单线程几乎总是持有锁的情况。
  2. 轻量级锁(Lightweight Locking)

    • 思想:当有另一个线程尝试获取锁时,偏向锁会升级为轻量级锁,竞争线程会通过自旋(忙等待)的方式尝试获取锁,而不是立即阻塞。
    • 场景:适用于线程竞争不激烈的情况,自旋避免了线程上下文切换的开销。
  3. 重量级锁(Heavyweight Locking)

    • 思想:当自
分享:
扫描分享到社交APP
上一篇
下一篇