0
点赞
收藏
分享

微信扫一扫

深入解析 Java 中的 Synchronized:原理与实战案例

一:概述

在 Java 并发编程中,synchronized是一个非常重要的关键字,用于解决多线程环境下的线程安全问题。它通过提供一种同步机制,确保多个线程在访问共享资源时不会出现数据不一致的情况。本文将详细介绍synchronized的底层实现原理,并通过多种实现方法和实际案例,帮助读者更好地理解和使用它。

二:具体说明

一、synchronized的基本概念

Synchronized是 Java 中用于控制多个线程对共享资源访问的同步机制。它主要有以下三种使用方式:

• 同步方法:通过在方法上添加synchronized关键字,使得整个方法成为一个同步方法。如果同步方法是实例方法,锁对象是当前实例(this);如果是静态方法,锁对象是类的Class实例。

• 同步代码块:通过在代码块上添加synchronized关键字,并指定锁对象,使得代码块内的代码成为同步代码块。

• 锁对象:通过将一个对象作为锁,多个线程在访问该对象时需要获取锁,从而实现同步。

二、synchronized的底层实现原理

Synchronized的实现基于 Java 虚拟机的监视器锁(Monitor)。监视器锁是一种基于对象的锁机制,每个 Java 对象都有一个与之关联的监视器锁。当线程进入同步代码块或同步方法时,它会尝试获取监视器锁;当线程退出同步代码块或同步方法时,它会释放监视器锁。

在字节码层面,synchronized的实现依赖于monitorentermonitorexit指令。当线程进入同步代码块时,会执行monitorenter指令,尝试获取锁;当线程退出同步代码块时,会执行monitorexit指令,释放锁。

三、synchronized的实现方法与案例

(一)同步方法

同步方法是最简单的synchronized使用方式。它通过在方法上添加synchronized关键字,使得整个方法成为一个同步方法。

1.实例方法同步

synchronized修饰实例方法时,锁对象是当前实例(this)。这意味着同一时刻只有一个线程可以执行该实例方法。

案例:模拟多个线程访问共享资源

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount());
    }
}

在这个案例中,increment方法被synchronized修饰,确保了多个线程在访问count变量时不会出现数据不一致的情况。最终输出的count值为 2000,说明线程安全得到了保证。

2.静态方法同步

synchronized修饰静态方法时,锁对象是类的Class实例。这意味着同一时刻只有一个线程可以执行该静态方法。

案例:模拟多个线程访问静态资源

public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

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

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                StaticCounter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + StaticCounter.getCount());
    }
}

在这个案例中,increment方法被synchronized修饰为静态方法,确保了多个线程在访问静态变量count时不会出现数据不一致的情况。最终输出的count值为 2000,说明线程安全得到了保证。

(二)同步代码块

同步代码块通过在代码块上添加synchronized关键字,并指定锁对象,使得代码块内的代码成为同步代码块。

1.锁定当前实例(this

当锁对象是当前实例(this)时,同步代码块的作用范围仅限于当前实例。

案例:模拟多个线程访问共享资源

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

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

    public static void main(String[] args) {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount());
    }
}

在这个案例中,increment方法中的同步代码块通过synchronized (this)锁定了当前实例,确保了多个线程在访问count变量时不会出现数据不一致的情况。最终输出的count值为 2000,说明线程安全得到了保证。

2.锁定类的Class实例

当锁对象是类的Class实例时,同步代码块的作用范围是整个类。

案例:模拟多个线程访问静态资源

public class StaticCounter {
    private static int count = 0;

    public void increment() {
        synchronized (StaticCounter.class) {
            count++;
        }
    }

    public static int getCount() {
        synchronized (StaticCounter.class) {
            return count;
        }
    }

    public static void main(String[] args) {
        StaticCounter counter = new StaticCounter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + StaticCounter.getCount());
    }
}

在这个案例中,increment方法中的同步代码块通过synchronized (StaticCounter.class)锁定了类的Class实例,确保了多个线程在访问静态变量count时不会出现数据不一致的情况。最终输出的count值为 2000,说明线程安全得到了保证。

(三)锁对象

锁对象是通过将一个对象作为锁,多个线程在访问该对象时需要获取锁,从而实现同步。

案例:模拟多个线程访问共享资源

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

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

    public static void main(String[] args) {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount());
    }
}

在这个案例中,increment方法中的同步代码块通过synchronized (lock)锁定了一个私有对象lock,确保了多个线程在访问count变量时不会出现数据不一致的情况。最终输出的count值为 2000。

四、synchronized的性能优化与锁优化机制

虽然synchronized是一种非常强大的同步机制,但在高并发场景下,它可能会带来一定的性能开销。为了优化性能,Java 虚拟机(JVM)对synchronized进行了一系列的锁优化机制,包括锁偏向、锁粗化、轻量级锁和自旋锁等。

(一)锁偏向

锁偏向是一种针对单线程场景的优化机制。它假设在大多数情况下,锁只会被一个线程访问,因此在第一次获取锁时,JVM 会将锁对象的 Mark Word 设置为指向该线程的偏向锁状态。如果后续的线程尝试获取该锁,且锁对象的 Mark Word 仍然指向当前线程,则可以直接进入同步代码块,而无需进行锁的获取和释放操作。

案例:锁偏向的性能优势

public class LockBiasingExample {
    private final Object lock = new Object();

    public void doWork() {
        synchronized (lock) {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        LockBiasingExample example = new LockBiasingExample();

        // 单线程场景
        for (int i = 0; i < 10000; i++) {
            example.doWork();
        }

        // 多线程场景
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                example.doWork();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,当只有一个线程访问doWork方法时,锁偏向机制会将锁对象的 Mark Word 设置为指向当前线程的偏向锁状态,从而避免了锁的获取和释放操作,提高了性能。然而,当多个线程同时访问时,锁偏向机制会失效,JVM 会将锁升级为轻量级锁或重量级锁。

(二)轻量级锁

轻量级锁是一种基于原子操作的锁机制,它通过使用 CAS(Compare-And-Swap)操作来尝试获取锁。如果锁对象的 Mark Word 当前未被锁定,线程会通过 CAS 操作将其设置为指向当前线程的轻量级锁状态。如果 CAS 操作成功,则线程获得锁并进入同步代码块;如果 CAS 操作失败,则线程会尝试自旋等待锁的释放。

案例:轻量级锁的实现

public class LightweightLockExample {
    private final Object lock = new Object();

    public void doWork() {
        synchronized (lock) {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        LightweightLockExample example = new LightweightLockExample();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                example.doWork();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,当多个线程同时访问doWork方法时,轻量级锁机制会通过 CAS 操作尝试获取锁。如果锁对象的 Mark Word 当前未被锁定,线程会通过 CAS 操作将其设置为指向当前线程的轻量级锁状态。如果 CAS 操作失败,则线程会尝试自旋等待锁的释放。如果自旋等待超过一定次数仍未获取到锁,JVM 会将锁升级为重量级锁。

(三)锁粗化

锁粗化是一种将多个连续的同步代码块合并为一个较大的同步代码块的优化机制。它通过减少锁的获取和释放次数,从而降低锁的开销。

案例:锁粗化的实现

public class LockCoarseningExample {
    private final Object lock = new Object();

    public void doWork() {
        synchronized (lock) {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        synchronized (lock) {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        LockCoarseningExample example = new LockCoarseningExample();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                example.doWork();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,doWork方法中有两个连续的同步代码块。JVM 会通过锁粗化机制将这两个同步代码块合并为一个较大的同步代码块,从而减少锁的获取和释放次数,提高性能。

(四)自旋锁

自旋锁是一种基于忙等待的锁机制,它通过让线程在等待锁释放时不断循环(自旋)来避免线程的阻塞和上下文切换。自旋锁适用于锁持有时间较短的场景,因为在这种情况下,线程自旋等待锁释放的时间通常比线程阻塞和上下文切换的时间要短。

案例:自旋锁的实现

public class SpinLockExample {
    private final Object lock = new Object();

    public void doWork() {
        synchronized (lock) {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SpinLockExample example = new SpinLockExample();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                example.doWork();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,当多个线程同时访问doWork方法时,轻量级锁机制会通过 CAS 操作尝试获取锁。如果 CAS 操作失败,则线程会尝试自旋等待锁的释放。如果自旋等待超过一定次数仍未获取到锁,JVM 会将锁升级为重量级锁。

五、synchronized的局限性与替代方案

尽管synchronized是一种非常强大的同步机制,但它也有一些局限性。例如,synchronized是可重入的,但不可中断;synchronized的性能在高并发场景下可能会受到影响;synchronized无法实现公平锁等。

为了克服这些局限性,Java 提供了一些替代方案,如java.util.concurrent.locks.ReentrantLockjava.util.concurrent包中的其他锁机制。

(一)ReentrantLock

ReentrantLock是一种可重入的锁机制,它提供了比synchronized更灵活的锁操作。ReentrantLock支持公平锁和非公平锁,可以中断线程的等待,还可以尝试非阻塞地获取锁。

案例:ReentrantLock的使用

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doWork() {
        lock.lock();
        try {
            // 模拟一些工作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                example.doWork();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,ReentrantLock通过lock()方法获取锁,并通过unlock()方法释放锁。与synchronized不同的是,ReentrantLock支持公平锁和非公平锁。如果需要公平锁,可以在创建ReentrantLock时传入true参数,例如:

private final ReentrantLock lock = new ReentrantLock(true);

此外,ReentrantLock还支持中断等待的线程。例如,如果一个线程在等待锁时被中断,它会抛出InterruptedException,从而可以更好地处理线程中断的情况。

(二)其他锁机制

除了ReentrantLock,Java 的java.util.concurrent.locks包中还提供了其他锁机制,如ReadWriteLockStampedLock等。这些锁机制可以根据不同的场景选择使用。

1.ReadWriteLock

ReadWriteLock是一种读写锁机制,允许多个线程同时读取共享资源,但在写入时会独占锁。这种锁机制适用于读多写少的场景。

案例:ReadWriteLock的使用

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int count = 0;

    public void increment() {
        lock.writeLock().lock();
        try {
            count++;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public int getCount() {
        lock.readLock().lock();
        try {
            return count;
        } finally {
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        Runnable readTask = () -> {
            for (int i = 0; i < 10000; i++) {
                example.getCount();
            }
        };

        Runnable writeTask = () -> {
            for (int i = 0; i < 10000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(readTask);
        Thread t2 = new Thread(readTask);
        Thread t3 = new Thread(writeTask);
        t1.start();
        t2.start();
        t3.start();

        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + example.getCount());
    }
}

在这个案例中,ReadWriteLock允许多个线程同时读取count,但在写入时会独占锁。这种锁机制在读多写少的场景下可以显著提高性能。

2.StampedLock

StampedLock是一种更灵活的锁机制,支持读锁、写锁和乐观读锁。乐观读锁允许线程在不获取锁的情况下读取共享资源,但如果在读取过程中资源被修改,则需要重新读取。

案例:StampedLock的使用

import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private final StampedLock lock = new StampedLock();
    private int count = 0;

    public void increment() {
        long stamp = lock.writeLock();
        try {
            count++;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public int getCount() {
        long stamp = lock.tryOptimisticRead();
        int count = this.count;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                count = this.count;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return count;
    }

    public static void main(String[] args) {
        StampedLockExample example = new StampedLockExample();

        Runnable readTask = () -> {
            for (int i = 0; i < 10000; i++) {
                example.getCount();
            }
        };

        Runnable writeTask = () -> {
            for (int i = 0; i < 10000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(readTask);
        Thread t2 = new Thread(readTask);
        Thread t3 = new Thread(writeTask);
        t1.start();
        t2.start();
        t3.start();

        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + example.getCount());
    }
}

在这个案例中,StampedLock支持乐观读锁,允许线程在不获取锁的情况下读取共享资源。如果在读取过程中资源被修改,则需要重新读取。

六、总结

Synchronized是 Java 中一种非常重要的同步机制,通过监视器锁(Monitor)实现线程安全。它可以通过同步方法、同步代码块和锁对象等多种方式实现。虽然synchronized是一种简单易用的同步机制,但在高并发场景下可能会带来一定的性能开销。为了优化性能,JVM 提供了锁偏向、轻量级锁、锁粗化和自旋锁等锁优化机制。

然而,synchronized也有一些局限性,例如不可中断、不支持公平锁等。为了克服这些局限性,Java 提供了ReentrantLock和其他锁机制,如ReadWriteLockStampedLock。这些锁机制提供了更灵活的锁操作和更高的性能,可以根据不同的场景选择使用。

在实际开发中,选择合适的同步机制非常重要。如果场景简单且对性能要求不高,synchronized是一个不错的选择;如果需要更灵活的锁操作和更高的性能,可以考虑使用ReentrantLock或其他锁机制。总之,理解synchronized的原理和使用方法,以及掌握其他锁机制的特点和应用场景,是每个 Java 开发者都应该具备的技能。

希望本文对您有所帮助!如果您对synchronized或其他锁机制有更多疑问,欢迎在评论区留言,我会为您解答!






举报

相关推荐

0 条评论