基于锁的获取与释放方式即计划于所得获取与释放方式进行分类——显式锁和隐式锁

隐式锁

Java中的隐式锁(也称为内置锁或自动锁)是通过使用关键字实现的一种线程同步机制。当一个线程进入被synchronized修饰的方法或代码块时,它会自动获得对象级别的锁,退出该方法或代码块时则会自动释放这把锁。

在Java中,隐式锁的实现机制主要包括以下两种类型:

  • 互斥锁(Mutex) 

虽然Java标准库并未直接暴露操作系统的互斥锁提供使用,但在Java虚拟机对synchronized关键字处理的底层实现中,当锁竞争激烈且必须升级为重量级锁时,会利用操作系统的互斥量机制来确保在同一时刻仅允许一个线程持有锁,从而实现严格的线程互斥控制。

  • 内部锁(Intrinsinc Lock)或监视器锁(Monitor Lock)

Java语言为每个对象内建了一个监视器锁,这是一个更高级别的抽象。我们可以通过使用synchronized关键字即可便捷地管理和操作这些锁。当一个线程访问被synchronized修饰的方法或代码块时,会自动获取相应对象的监视器锁,并在执行完毕后自动释放,这一过程对用户透明,故被称为隐式锁。每个Java对象均与一个监视器锁关联,以此来协调对该对象状态访问的并发控制。 

应用场景分为:

  • 实例方法同步:锁是当前实例对象。
  • 静态方法同步:锁是当前类的Class对象。
  • 同步代码块(使用对象实例作为锁)。
  • 同步代码块(使用类的Class对象作为锁)

场景1: 对象状态保护(账户余额操作)

public class BankAccount {
    private double balance; // 共享资源

    // 隐式锁保护存款操作(锁为当前对象实例)
    public synchronized void deposit(double amount) {
        balance += amount;
        System.out.println("存款: +" + amount + " | 余额: " + balance);
    }

    // 隐式锁保护取款操作
    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println("取款: -" + amount + " | 余额: " + balance);
        } else {
            System.out.println("取款失败!余额不足");
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        
        // 多线程并发操作账户
        new Thread(() -> account.deposit(100)).start();
        new Thread(() -> account.withdraw(50)).start();
        new Thread(() -> account.withdraw(70)).start();
    }
}

输出结果:

存款: +100.0 | 余额: 100.0
取款: -50.0 | 余额: 50.0
取款失败!余额不足 

说明:

  • synchronized方法保证同一时间只有一个线程能操作balance
  • 自动处理锁的获取/释放,避免余额不一致。

 场景2:静态方法锁(类级别锁)

public class IdGenerator {
    private static int counter = 0;

    // 静态方法锁(锁为Class对象)
    public static synchronized int generateId() {
        return ++counter;
    }

    public static void main(String[] args) {
        // 多线程调用静态方法
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int id = IdGenerator.generateId();
                System.out.println(Thread.currentThread().getName() + " : " + id);
            }).start();
        }
    }
}

输出结果:顺序可能不同,但ID不重复

Thread-0 : 1
Thread-2 : 3
Thread-1 : 2
...

说明:

  • 静态方法锁保护类级别的共享变量counter
  • 锁对象是IdGenerator.class,所有实例共享同一把锁。

对于静态方法锁(类级别锁)典型应用场景全局唯一ID生成器,单例模式实现(线程安全版),全局配置管理,跨实例的共享资源控制

全局唯一ID生成器
  • 场景:多线程环境下生成不重复的订单号、用户ID等
  • 问题:普通对象锁无法保护静态计数器
  • 解决方案:类锁保护静态变量
public class OrderIdGenerator {
    private static int counter = 0;

    // 类级别锁保护
    public static synchronized String generateId() {
        return "ORD-" + System.currentTimeMillis() + "-" + (++counter);
    }
}

// 多线程安全调用
IntStream.range(0, 10).forEach(i -> 
    new Thread(() -> {
        System.out.println(OrderIdGenerator.generateId());
    }).start()
);
单例模式实现(线程安全版)
  • 场景:延迟初始化重量级资源(数据库连接池、配置加载)
  • 问题:双重检查锁需配合类锁使用
public class DatabasePool {
    private static volatile DatabasePool instance;

    // 私有构造方法
    private DatabasePool() { /* 初始化连接池 */ }

    // 类锁保护实例创建
    public static DatabasePool getInstance() {
        if (instance == null) {
            synchronized (DatabasePool.class) { // 类级别锁
                if (instance == null) {
                    instance = new DatabasePool();
                }
            }
        }
        return instance;
    }
}
 全局配置管理
  • 场景:多线程加载/刷新应用配置
  • 问题:配置需保证全局一致性
public class AppConfig {
    private static Properties config;

    // 类锁保护配置加载
    public static synchronized void reloadConfig() {
        Properties newConfig = new Properties();
        try (InputStream is = new FileInputStream("config.properties")) {
            newConfig.load(is);
            config = newConfig; // 原子性更新
        }
    }

    // 读操作无需同步(final字段可见性保证)
    public static String get(String key) {
        return config.getProperty(key);
    }
}
跨实例的共享资源控制
  • 场景:限制整个类的某个行为(如全局限流)
  • 问题:需要跨对象实例协同
public class RateLimiter {
    private static int requestCount = 0;
    private static final int MAX_REQUESTS = 100;

    // 类锁保护全局计数器
    public static synchronized boolean allowRequest() {
        if (requestCount >= MAX_REQUESTS) {
            return false;
        }
        requestCount++;
        return true;
    }

    public static synchronized void releaseRequest() {
        requestCount--;
    }
}

场景3:同步代码块(减小细粒度)

public class Cache {
    private final Object lock = new Object(); // 专用锁对象
    private Map data = new HashMap<>();

    public void update(String key, String value) {
        // 非同步操作(无竞争)
        System.out.println("预处理...");

        // 仅同步共享资源操作
        synchronized (lock) {
            data.put(key, value);
            System.out.println("更新: " + key + "=" + value);
        }
    }

    public String get(String key) {
        synchronized (lock) {
            return data.get(key);
        }
    }
}

 对于同步代码块(减小细粒度)典型应用场景,缓存系统(读写分离),订单状态变更(局部同步),连接池管理(减少锁范围),批量处理(分段加锁)

缓存系统(读写分离)
  • 场景:缓存需要高频读写,但只有写操作需要同步
  • 问题:直接锁整个方法会导致读操作也被阻塞
  • 优化:用同步块只保护写操作
public class CacheSystem {
    private final Map cache = new HashMap<>();
    private final Object lock = new Object(); // 专用锁对象

    // 读操作(不加锁)
    public String get(String key) {
        return cache.get(key); 
    }

    // 写操作(仅同步必要部分)
    public void update(String key, String value) {
        // 非同步预处理(如参数校验)
        if (key == null) throw new IllegalArgumentException();

        synchronized (lock) { // 只锁核心逻辑
            cache.put(key, value);
            System.out.println("Updated: " + key);
        }
    }
}
订单状态变更(局部同步)
  • 场景:订单状态变更需保证原子性,但其他操作(如日志记录)无需同步
  • 问题:锁整个方法会导致无关操作阻塞
  • 优化:只同步状态变更部分
public class OrderService {
    private Order currentOrder;

    public void processOrder(Order newOrder) {
        // 非关键操作(如日志)
        log("Processing order: " + newOrder.getId());

        // 仅同步状态变更
        synchronized (this) {
            if (currentOrder == null) {
                currentOrder = newOrder;
                updateDatabase(newOrder); // 关键操作
            }
        }
    }

    private void log(String message) { /* 非同步操作 */ }
}
连接池管理(减少锁范围)
  • 场景:获取连接时需要同步,但连接使用过程无需锁
  • 问题:锁整个getConnection()方法会导致连接使用期间也持有锁
  • 优化:仅同步连接分配逻辑
public class ConnectionPool {
    private final LinkedList pool = new LinkedList<>();
    private final Object poolLock = new Object();

    public Connection getConnection() throws SQLException {
        Connection conn;
        synchronized (poolLock) { // 仅同步分配过程
            if (pool.isEmpty()) {
                conn = createNewConnection();
            } else {
                conn = pool.removeFirst();
            }
        }
        
        // 连接初始化(非同步操作)
        conn.setAutoCommit(true);
        return conn;
    }
}
批量处理(分段加锁)
  • 场景:批量处理集合数据时,不同元素可独立处理
  • 问题:锁整个集合会导致并行度下降
  • 优化:按元素分段加锁
public class BatchProcessor {
    private final List taskQueue = new ArrayList<>();
    private final Object[] segmentLocks = new Object[16]; // 分段锁数组

    public BatchProcessor() {
        Arrays.setAll(segmentLocks, i -> new Object());
    }

    public void processTask(int taskId) {
        Object task = taskQueue.get(taskId);
        Object lock = segmentLocks[taskId % 16]; // 选择分段锁

        synchronized (lock) { // 只锁当前分段
            // 处理任务(同一分段的task串行,不同分段可并行)
            System.out.println("Processing task: " + taskId);
        }
    }
} 
  

隐式锁的优缺点

  • 优点
  1. 简洁易用:程序员无需手动管理锁的获取和释放过程,降低了编程复杂性。
  2. 安全性:隐式锁确保了线程安全,避免了竞态条件,因为一次只有一个线程能持有锁并执行同步代码块。
  3. 异常处理下的自动释放:即使在同步代码块中抛出异常,隐式锁也会在异常退出时被释放,防止死锁。
  • 缺点
  1. 锁定粒度:隐式锁的粒度通常是对象级别,这意味着如果一个大型对象的不同部分实际上可以独立地被不同线程访问,但由于整个对象被锁定,可能导致不必要的阻塞和较低的并发性能。
  2. 不灵活:相对于显示锁(如java.util.concurrent.locks.Lock接口的实现类),隐式锁的功能较有限,无法提供更细粒度的控制,如尝试获取锁、定时等待、可中断的获取锁等高级特性。
  3. 锁竞争影响:在高并发环境下,若多个线程竞争同一把锁,可能会引发“锁争用”,导致性能下降,特别是在出现锁链和死锁的情况下。

隐式锁的适用场景

        隐式锁适用于相对简单的多线程同步需求,尤其是在只需要保护某个对象状态完整性,且无需过多关注锁策略灵活性的场合。

显示锁

显式锁是由java.util.concurrent.locks.Lock接口及其诸多实现类提供的同步机制,相较于通过synchronized关键字实现的隐式锁机制,显式锁赋予开发者更为精细和灵活的控制能力,使其能够在多线程环境中精准掌控同步动作。显式锁的核心作用在于确保在任何时刻仅有一个线程能够访问关键代码段或共享数据,从而有效防止数据不一致性问题和竞态条件。

相较于隐式锁,显式锁提供了更为多样化的锁操作选项,包括但不限于支持线程在等待锁时可被中断、根据先后顺序分配锁资源的公平锁与非公平锁机制,以及能够设定锁获取等待时间的定时锁功能。这些特性共同增强了显式锁在面对复杂并发场景时的适应性和可调控性,使之成为解决高度定制化同步需求的理想工具。

日常开发中,常见的显式锁分类有如下几种:

  • ReentrantLock:可重入锁

可重入锁,继承自Lock接口,支持可中断锁、公平锁和非公平锁的选择。可重入意味着同一个线程可以多次获取同一线程持有的锁。 

  • ReentrantReadWriteLock:读写锁

读写锁,提供了两个锁,一个是读锁,允许多个线程同时读取;另一个是写锁,同一时间内只允许一个线程写入,写锁会排斥所有读锁和写锁。 

  • StampedLock 

带版本戳的锁,提供了乐观读、悲观读写模式,适合于读多写少的场景,可以提升系统性能。 

应用场景分为: 

  • ReentrantLock:可重入锁(银行账户转账场景)
  • ReentrantReadWriteLock:读写锁(商品库存缓存)
  • StampedLock:乐观读锁(实时股票报价系统)

场景 1:ReentrantLock:可重入锁(银行账户转账场景)

  • 场景:多线程转账操作需保证原子性,支持转账超时中断(防止死等)
  • 核心需求
    • 防止转账金额不一致,
    • 线程等待锁超过3秒则放弃并记录日志,
    • 支持线程中断响应
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BankAccount {
    private final Lock lock = new ReentrantLock(); // 默认非公平锁
    private int balance;

    public BankAccount(int balance) {
        this.balance = balance;
    }

    // 转账操作(支持超时与中断)
    public boolean transfer(BankAccount to, int amount) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        while (true) {
            if (this.lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取转出账户锁
                try {
                    if (to.lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取转入账户锁
                        try {
                            if (balance >= amount) {
                                balance -= amount;
                                to.balance += amount;
                                return true;
                            }
                            return false; // 余额不足
                        } finally {
                            to.lock.unlock();
                        }
                    }
                } finally {
                    this.lock.unlock();
                }
            }

            // 总等待超时判定
            if (System.currentTimeMillis() - startTime > 3000) {
                System.out.println(Thread.currentThread().getName() + ": 转账超时放弃");
                return false;
            }
            Thread.sleep(100); // 避免CPU忙等
        }
    }
}

// 使用示例
public class TransferDemo {
    public static void main(String[] args) {
        BankAccount acc1 = new BankAccount(1000);
        BankAccount acc2 = new BankAccount(500);

        new Thread(() -> {
            try {
                acc1.transfer(acc2, 200); // 线程1: acc1 -> acc2转200
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-1").start();

        new Thread(() -> {
            try {
                acc2.transfer(acc1, 100); // 线程2: acc2 -> acc1转100
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-2").start();
    }
}

场景 2:ReentrantReadWriteLock:读写锁(商品库存缓存)

  • 场景:高频读取商品库存,低频更新库存(读多写少)
  • 核心需求
    • 允许多线程并发读取库存,
    • 库存更新时阻塞所有读/写操作,
    • 避免「写饥饿」
import java.util.concurrent.locks.*;

class ProductInventory {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private int stock = 100; // 初始库存

    // 读操作:多个线程可同时执行
    public int getStock() {
        readLock.lock();
        try {
            return stock;
        } finally {
            readLock.unlock();
        }
    }

    // 写操作:独占访问
    public void updateStock(int newStock) {
        writeLock.lock();
        try {
            stock = newStock;
        } finally {
            writeLock.unlock();
        }
    }
}

// 使用示例
public class InventoryDemo {
    public static void main(String[] args) {
        ProductInventory inventory = new ProductInventory();

        // 10个读线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() 
                    + " 读取库存: " + inventory.getStock());
            }, "Reader-" + i).start();
        }

        // 1个写线程(更新库存)
        new Thread(() -> {
            inventory.updateStock(80);
            System.out.println("库存更新为: 80");
        }, "Writer").start();
    }
}

场景 3:StampedLock:乐观读锁(实时股票报价系统)

  • 场景:99%时间读取股票价格,1%时间更新价格
  • 核心需求
    • 无竞争时完全无锁读取
    • 写操作发生时快速切换为悲观锁
    • 避免读操作阻塞写操作
import java.util.concurrent.locks.StampedLock;

class StockQuote {
    private final StampedLock sl = new StampedLock();
    private double price = 100.0; // 初始价格

    // 乐观读:无锁快照读取
    public double getCurrentPrice() {
        long stamp = sl.tryOptimisticRead(); // 获取乐观读戳
        double currentPrice = this.price;    // 读取共享变量
        
        // 检查期间是否有写操作发生
        if (!sl.validate(stamp)) {
            stamp = sl.readLock(); // 升级为悲观读锁
            try {
                currentPrice = price;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return currentPrice;
    }

    // 写操作:独占访问
    public void updatePrice(double newPrice) {
        long stamp = sl.writeLock();
        try {
            price = newPrice;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

// 使用示例
public class StockQuoteDemo {
    public static void main(String[] args) {
        StockQuote quote = new StockQuote();

        // 读线程(高频)
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() 
                    + " 报价: " + quote.getCurrentPrice());
            }, "Reader-" + i).start();
        }

        // 写线程(低频)
        new Thread(() -> {
            quote.updatePrice(105.5);
            System.out.println("价格更新为: 105.5");
        }, "Writer").start();
    }
}

显示锁的优缺点:

  • 优点
  1. 灵活控制:显式锁提供了多种获取和释放锁的方式,可以根据实际需求进行选择,比如中断等待锁的线程,设置超时获取锁等。
  2. 性能优化:在某些特定场景下,显式锁可以提供比隐式锁更好的性能表现,尤其是当需要避免死锁或优化读多写少的情况时。
  3. 公平性选择:显式锁允许创建公平锁,按照线程请求锁的顺序给予响应,保证所有线程在等待锁时有一定的公平性。
  • 缺点
  1. 使用复杂:相较于隐式锁,显式锁需要手动调用 lock() 和 unlock() 方法,增加了编程复杂性,如果不正确地使用(如忘记释放锁或未捕获异常导致锁未释放),容易造成死锁或其他并发问题。
  2. 性能开销:在某些简单场景下,显式锁的额外API调用和锁状态管理可能带来额外的性能开销,尤其当公平锁启用时,由于需要维护线程队列和线程调度,可能会影响整体性能。
  3. 错误可能性:由于显式锁的操作更加细致,因此更容易出错,开发者需要具备较高的并发编程意识和技能才能妥善使用。

显示锁的使用场景: 

ReentrantLock (可重入锁) 的使用场景
  1.  需要替代 synchronized 但要求更多功能:
    • 可中断的锁等待: 当线程A在等待线程B持有的锁时,如果线程A需要被及时响应中断(比如系统关闭、用户取消操作),使用lockInterruptibly()方法可以中断等待,避免线程无限期阻塞。这在synchronized中无法实现。
    • 尝试非阻塞获取锁: 使用tryLock()方法。如果锁当前不可用,线程可以立即返回去做其他事情(例如尝试获取其他资源、记录日志、提供备选方案),而不是傻等。场景示例: 避免死锁(尝试按特定顺序获取多个锁,失败则释放已获得的锁并重试或放弃)。
    • 具有超时的锁获取: 使用tryLock(long time, TimeUnit unit)方法。线程只愿意等待锁一定的时间,超时后放弃获取锁。场景示例: 访问需要保证响应时间的资源(如数据库连接池、外部服务调用),防止线程因锁争用导致整体系统响应变慢。 
    • 公平锁需求: 通过构造器参数new ReentrantLock(true)创建公平锁,保证等待时间最长的线程优先获得锁。场景示例: 对线程获取锁的顺序有严格要求,避免线程饥饿(如某些低优先级线程永远拿不到锁)。注意:公平锁通常性能低于非公平锁。 
  2. 需要细粒度控制锁的获取和释放:
    • Lock接口明确要求lock()unlock()必须成对出现,且unlock()通常放在finally块中确保释放。这种显式控制比synchronized块更灵活,尤其在跨越多个方法或需要根据复杂条件释放锁时。

ReentrantReadWriteLock (读写锁) 的使用场景,核心解决 “读多写少” 场景下的性能瓶颈。
  1. 共享数据的缓存:
    1. 场景描述: 一个缓存(如内存缓存、配置信息缓存)被大量线程频繁读取,但更新(写入)操作相对较少。
    2. 解决方案: 使用读写锁。多个线程可以同时持有读锁进行读取操作(并发读),极大提升读取吞吐量。当需要更新缓存时,线程需要获取写锁。写锁是独占的,获取时会阻塞所有后续的读锁和写锁请求,确保写入时数据的强一致性。写入完成后释放写锁,所有等待的读/写线程可以继续竞争。
    3. 优势: 相比使用单个互斥锁(如synchronizedReentrantLock),读写锁在读取密集型场景下能显著提高并发性能。
  2. 资源状态查询与修改: 
    1. 场景描述: 一个共享资源(如数据库连接池状态、大型数据结构Map/Set的状态)经常被查询(读操作),但修改其状态(如添加/移除连接、更新结构)的操作较少。
    2. 解决方案: 同样利用读写锁。查询状态时获取读锁(允许多个查询并发);修改状态时获取写锁(独占,确保修改原子性和一致性)。
StampedLock 的使用场景

在 “读非常多,写非常少” 且对读性能要求极高的场景下,它是ReentrantReadWriteLock的一个更高级、更激进的替代方案。它引入了乐观读模式。

  1. 极度读取密集型数据结构: 
    1. 场景描述: 存在一个被海量线程频繁访问(主要是读)的数据结构(如实时股票报价快照、游戏世界状态、大规模只读配置),写操作极其罕见(可能只有初始化或偶尔的配置热更新)。
    2. 解决方案: 优先使用tryOptimisticRead()进行乐观读
      1. 乐观读不阻塞任何线程(包括写线程!),它只是获取一个“戳记”(Stamp)。
      2. 线程读取所需数据。
      3. 读取完成后,使用validate(stamp)检查在读取过程中是否有写操作发生。
      4. 如果validate返回true(没有写操作干扰),则乐观读成功,读取的数据有效。
      5. 如果validate返回false(有写操作发生),则升级为标准的悲观读锁 (readLock()) 或写锁 (writeLock()),然后重新读取数据以确保一致性
    3. 优势: 在绝大多数没有写操作的情况下,乐观读完全避免了锁的开销,提供了接近无锁的读取性能,这是读写锁无法比拟的。只有在发生罕见的读写冲突时才退化成较慢的悲观锁模式。
    4. 注意事项: 乐观读适合读取的数据结构相对简单、原子性读取容易(如读取几个基本类型字段或引用)。如果读取过程复杂或需要读取多个关联字段并保持一致性视图,悲观读锁可能更安全简单。StampedLock 不可重入,使用不当容易死锁,且没有条件变量(Condition)的直接支持。
对比
锁类型 核心特性 最佳适用场景 关键优势 注意事项
ReentrantLock 可重入、可中断、可超时、可公平 需要比synchronized更精细控制的同步;需要尝试获取锁或超时;需要公平性 功能丰富,控制灵活 必须手动unlock();公平锁性能稍差
ReentrantReadWriteLock 读锁共享、写锁独占 读多写少的共享数据访问(缓存、资源状态查询) 显著提升读取密集型场景的并发性能 注意锁升级问题(持有读锁时不能直接获取写锁)
StampedLock 乐观读、悲观读、写锁;带版本戳 读非常多,写极少且对读性能要求极端高 乐观读模式提供接近无锁的读性能,大幅提升吞吐量 不可重入;API更复杂;乐观读需验证;无Condition

选择显示锁的一般原则:

  1. 优先考虑 synchronized 如果简单的互斥能满足需求,且不需要可中断、超时、尝试获取、公平性等高级特性,synchronized通常是更简洁、更不容易出错的选择(JVM内部优化也在持续改进)。
  2. 需要高级功能选 ReentrantLock 当需要可中断、超时、尝试获取、公平性控制,或者锁的获取/释放逻辑跨越复杂代码块(不能简单地用synchronized块包裹)时。
  3. 读多写少选读写锁: 当存在明显的读取远多于写入的场景,并且性能瓶颈在于读取并发度时。
  4. 极致读性能且写极少选 StampedLock 当读操作是性能的绝对关键路径,写操作极其罕见,并且愿意承担更复杂的API和潜在的重试开销来换取最高读取吞吐量时。评估乐观读失败的概率和重试成本非常重要。

你可能感兴趣的:(Java,java)