Java 线程安全:synchronized 与 Lock 的区别、代码示例及高并发场景适配

摘要:在Java中,synchronizedLock都是实现线程同步的机制,用于解决多线程并发访问共享资源时的线程安全问题。但它们在实现原理、功能特性和使用场景上有显著区别,下面从多个维度详细对比并结合示例说明

一、核心区别总览

维度 synchronized Lock(以ReentrantLock为例)
实现层面 Java关键字,JVM原生实现 接口(java.util.concurrent.locks.Lock),API层面实现(如ReentrantLock
锁的获取与释放 自动获取/释放(代码块结束或异常时) 手动获取(lock())/释放(unlock()),需在finally中确保释放
中断响应 不可中断(等待锁的线程会一直阻塞) 可中断(lockInterruptibly()
超时获取 不支持 支持(tryLock(long timeout, TimeUnit unit)
公平性 仅支持非公平锁 可选择公平/非公平锁(构造函数参数指定)
绑定条件 wait()/notify()绑定,仅一个条件 可通过Condition绑定多个条件
读写分离 不支持 可通过ReadWriteLock实现读写分离
锁状态查询 无法查询 可查询(如isLocked()hasQueuedThreads()

二、详细区别及示例

1. 实现层面与基本用法
  • synchronized是Java关键字,由JVM直接管理,无需手动处理锁的释放。
  • Lock是接口,需通过实现类(如ReentrantLock)使用,锁的释放必须手动完成。

示例1:synchronized的基本用法

// 同步方法
class SynchronizedCounter {
    private int count = 0;
    
    // 同步方法(锁为当前对象)
    public synchronized void increment() {
        count++;
    }
    
    // 同步代码块(锁为指定对象)
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
    
    public synchronized int getCount() {
        return count;
    }
}

示例2:Lock的基本用法

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock(); // 实例化Lock
    
    public void increment() {
        lock.lock(); // 手动获取锁
        try {
            count++; // 临界区
        } finally {
            lock.unlock(); // 必须在finally中释放锁,避免死锁
        }
    }
    
    public void decrement() {
        lock.lock();
        try {
            count--;
        } finally {
            lock.unlock();
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

2. 锁的释放机制

  • synchronized:当代码块执行完毕或抛出异常时,JVM会自动释放锁,无需手动干预。
  • Lock:必须通过unlock()手动释放锁,且通常需要在finally块中执行(否则可能因异常导致锁未释放,引发死锁)。

3. 中断响应能力

  • synchronized:等待锁的线程无法被中断,会一直阻塞直到获取到锁。
  • Lock:支持中断等待锁的线程(通过lockInterruptibly()),可避免线程无限期阻塞。

示例3:Lock的中断响应

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptExample {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                // 尝试获取锁,允许被中断
                lock.lockInterruptibly();
                try {
                    System.out.println("线程1获取到锁,开始休眠");
                    Thread.sleep(10000); // 持有锁10秒
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                System.out.println("线程1等待锁时被中断");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                lock.lockInterruptibly(); // 尝试获取锁(此时t1持有锁)
                try {
                    System.out.println("线程2获取到锁");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                System.out.println("线程2等待锁时被中断");
            }
        });

        t1.start();
        Thread.sleep(1000); // 确保t1先获取锁
        t2.start();
        Thread.sleep(2000); // 让t2进入等待锁状态
        t2.interrupt(); // 中断t2的等待
    }
}

输出

线程1获取到锁,开始休眠
线程2等待锁时被中断
  • 说明:t2在等待锁时被中断,lockInterruptibly()会抛出InterruptedException,避免t2无限期阻塞。

4. 超时获取锁

  • synchronized:不支持超时,线程会一直阻塞直到获取锁。
  • Lock:通过tryLock(long timeout, TimeUnit unit)实现超时获取,若超时未获取到锁则返回false,可避免死锁。

示例4:Lock的超时获取

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTimeoutExample {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程1获取到锁,休眠5秒");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                // 尝试在2秒内获取锁
                boolean acquired = lock.tryLock(2, TimeUnit.SECONDS);
                if (acquired) {
                    try {
                        System.out.println("线程2获取到锁");
                    } finally {
                        lock.unlock();
                    }
                } else {
                    System.out.println("线程2超时未获取到锁,放弃等待");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

输出

线程1获取到锁,休眠5秒
线程2超时未获取到锁,放弃等待
  • 说明:t2尝试在2秒内获取锁,但t1持有锁5秒,因此t2超时后放弃,避免无限阻塞。

5. 公平性选择

  • synchronized:仅支持非公平锁(线程获取锁的顺序不保证与请求顺序一致,可能有"插队"现象)。
  • LockReentrantLock的构造函数可指定fair参数(true为公平锁,false为非公平锁),公平锁会按线程请求顺序分配锁(性能略低,但避免饥饿)。

示例5:Lock的公平锁

import java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
    // 公平锁(按请求顺序分配)
    private static final ReentrantLock fairLock = new ReentrantLock(true);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) { // 每个线程尝试获取2次锁
                    fairLock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " 获取到锁");
                    } finally {
                        fairLock.unlock();
                    }
                }
            }, "线程" + i).start();
        }
    }
}

输出(大致顺序)

线程0 获取到锁
线程1 获取到锁
线程2 获取到锁
线程3 获取到锁
线程4 获取到锁
线程0 获取到锁
线程1 获取到锁
...(按请求顺序执行)
  • 说明:公平锁保证线程按请求顺序获取锁,而非公平锁可能出现后请求的线程先获取锁。

6. 绑定多个条件

  • synchronized:与Objectwait()/notify()/notifyAll()绑定,一个锁只能关联一个条件队列(所有等待线程共享)。
  • Lock:通过newCondition()创建多个Condition对象,可实现多个条件队列(不同线程等待不同条件,唤醒时更精准)。

示例6:Lock的多条件绑定

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 生产者-消费者模型(多条件)
public class MultiConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition(); // 非空条件
    private final Condition notFull = lock.newCondition();  // 非满条件
    private final Object[] buffer = new Object[10];
    private int count = 0, putIndex = 0, takeIndex = 0;

    // 生产
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await(); // 缓冲区满,等待非满条件
            }
            buffer[putIndex] = x;
            if (++putIndex == buffer.length) putIndex = 0;
            count++;
            notEmpty.signal(); // 唤醒等待非空条件的消费者
        } finally {
            lock.unlock();
        }
    }

    // 消费
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 缓冲区空,等待非空条件
            }
            Object x = buffer[takeIndex];
            if (++takeIndex == buffer.length) takeIndex = 0;
            count--;
            notFull.signal(); // 唤醒等待非满条件的生产者
            return x;
        } finally {
            lock.unlock();
        }
    }
}
  • 说明:通过notEmptynotFull两个条件,生产者和消费者可分别等待不同条件,唤醒时仅通知相关线程,比synchronizednotifyAll()更高效。

7. 读写分离锁

  • synchronized:不支持读写分离,读操作和写操作互斥(即使多个读线程也无法并发)。
  • Lock:通过ReentrantReadWriteLock实现读写分离,多个读线程可并发访问(读锁共享),写线程独占(写锁排他),适合读多写少场景。

示例7:ReadWriteLock的读写分离

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

public class ReadWriteLockExample {
    private int data = 0;
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();  // 读锁(共享)
    private final Lock writeLock = rwLock.writeLock(); // 写锁(排他)

    // 读操作(共享)
    public int read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 读取数据: " + data);
            return data;
        } finally {
            readLock.unlock();
        }
    }

    // 写操作(排他)
    public void write(int newData) {
        writeLock.lock();
        try {
            data = newData;
            System.out.println(Thread.currentThread().getName() + " 写入数据: " + data);
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        
        // 3个读线程并发
        for (int i = 0; i < 3; i++) {
            new Thread(example::read, "读线程" + i).start();
        }
        
        // 1个写线程
        new Thread(() -> example.write(100), "写线程").start();
    }
}

输出(可能)

读线程0 读取数据: 0
读线程1 读取数据: 0
读线程2 读取数据: 0
写线程 写入数据: 100
  • 说明:多个读线程可同时获取读锁并发执行,而写线程必须等待所有读锁释放后才能获取写锁,提高了读操作的并发效率。

三、性能对比

  • 低并发场景:synchronized性能更好(JVM对其有优化,如偏向锁、轻量级锁)。
  • 高并发场景:Lock(如ReentrantLock)性能更稳定(减少了synchronized的重量级锁竞争开销)。

四、适用场景

  • 优先用synchronized:简单同步场景(如单一路径的锁竞争),代码简洁,无需手动管理锁释放。
  • 优先用Lock
    • 需要中断等待锁的线程、超时获取锁。
    • 需要公平锁或多条件绑定。
    • 读多写少场景(用ReadWriteLock)。
    • 需要查询锁状态(如isLocked())。

总结

synchronized是JVM原生的简单同步机制,适合基础场景;Lock是更灵活的API级同步工具,支持中断、超时、公平性等高级特性。实际开发中需根据场景选择,两者并非替代关系,而是互补关系。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐