Java高级上锁机制:显式锁 ReentrantLock

简介: Java 5.0 加入了新的上锁工作:ReentrantLock,它和同步(Synchronized)方法的内置锁不同,这是一种显式锁。显式锁作为一种高级的上锁工作, 是同步方法的一种补充和扩展,用来实现同步代码块无法完成的功能。

Java 5.0 加入了新的上锁工作:ReentrantLock,它和同步(Synchronized)方法的内置锁不同,这是一种显式锁。显式锁作为一种高级的上锁工作, 是同步方法的一种补充和扩展,用来实现同步代码块无法完成的功能。

1 Lock和ReentrantLock

Lock作为显式锁,其提供了一种无条件的、可轮询和定时的、可中断的锁操作,其获得锁和释放锁的操作都是显示。

Lock是Java 5.0 中加入的接口,表示显式锁的功能,其接口定义如下:

public interface Lock {
    void lock(); //获取锁
    void lockInterruptibly() throws InterruptedException; //可中断的获取锁操作
    boolean tryLock(); //尝试获取锁,不会被拥塞,如果失败立刻返回
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //在一定时间内尝试获得锁,如果超时则失败
    void unlock(); // 释放锁
    Condition newCondition();
}

前文中,我们已经讨论过,显式锁和同步代码块中的内置锁有着相同的互斥性和内存可见性。ReentrantLockLock的一种实现,提供对于线程的重入机制。和同步方法(Synchronized)相比,有着更强性能和灵活性。

虽然同步方法的内置锁已经很强大和完备了,但是在功能上还有一定的局限性:不能实现非拥塞的锁操作。比如不能提供响应中断的获得锁操作,不能提供支持超时的获得锁操作等等。因此,在某些情况下需要使用更为灵活的加锁方式,也就是显式锁。

在Java官方的注解中,给出了这样的代码示例:

 Lock l = new ReentrantLock();
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }

显式锁需要在手动调用lock方法来获得锁,并在使用后在finally代码块中调用unlock方法释放锁,以保证无论操作是否成功都能释放掉锁。

显式锁支持非拥塞的锁操作,具体的功能有:支持可轮询和定时的、以及可中断的锁获得操作。

1.1 轮询锁和定时锁

使用tryLock方法可以用于实现轮询锁定时锁。和无条件的获得锁操作相比,tryLock方法具有更完善的错误恢复机制,可以避免死锁的放生。相比之下,同步方法发生死锁,其恢复方法就只能重新启动程序。

避免死锁的方式之一为打破“请求与保持条件”(死锁的四个条件),比如在要获得多个锁才能工作的情况下,如果不能获得全部的锁,就会释放掉已经持有的锁,一段时间之后再去重新尝试获得所有的锁。也就是说要么获得所有锁,要么一个锁都不占有

下面的代码中以转账为例,演示了轮询锁的工作机制。

public class DeadlockAvoidance {
    private static Random rnd = new Random();

    // 转账
    public boolean transferMoney(Account fromAcct, //转出账户
                                 Account toAcct, //转入账户
                                 DollarAmount amount, //金额
                                 long timeout, //超时时间
                                 TimeUnit unit) 
            throws InsufficientFundsException, InterruptedException {
        long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
        long randMod = getRandomDelayModulusNanos(timeout, unit);
        long stopTime = System.nanoTime() + unit.toNanos(timeout);

        while (true) {
            // 尝试获得fromAcct的锁
            if (fromAcct.lock.tryLock()) {
                try {
                    // 尝试获得toAcct的锁
                    if (toAcct.lock.tryLock()) {
                        try {
                            if  (fromAcct.getBalance().compareTo(amount) < 0) //余额不足
                                throw new InsufficientFundsException();
                            else { // 余额满足,转账
                                fromAcct.debit(amount);
                                toAcct.credit(amount);
                                return true;
                            }
                        } finally { //释放toAcct锁
                            toAcct.lock.unlock();
                        }
                    }
                } finally { //释放fromAcct锁
                    fromAcct.lock.unlock();
                }
            }
            // 获得锁失败
            // 判断是否超时 如果超时则立刻失败
            if (System.nanoTime() < stopTime)
                return false;

            // 如果没有超时,随机睡眠一段时间
            NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
        }
    }


    class Account {
        //显示锁
        public Lock lock;

        void debit(DollarAmount d) {
        }

        void credit(DollarAmount d) {
        }

        DollarAmount getBalance() {
            return null;
        }
    }

    class InsufficientFundsException extends Exception {
    }
}

只有同时获得转出账户和转入账户的锁后,才会进行转账。如果不能同时获得两个锁,就释放掉已经获得的锁,并随机随眠一段时间,再去尝试获得全部的锁,循环这个过程直到超时。

除了轮询申请获得锁之外,也可以使用带有时间限制的定时锁操作,即获得锁的操作具有时间限制,超过一定时间后仍没有获得锁就会返回失败。示例如下:

public class TimedLocking {
    private Lock lock = new ReentrantLock();

    public boolean trySendOnSharedLine(String message,
                                       long timeout, TimeUnit unit)
            throws InterruptedException {
        // 设定超时时间
        long nanosToLock = unit.toNanos(timeout)
                - estimatedNanosToSend(message);
        // 在规定时间内等待锁 否者就会返回false
        if (!lock.tryLock(nanosToLock, NANOSECONDS))
            return false;
        try {
            return sendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }

    private boolean sendOnSharedLine(String message) {
        /* send something */
        return true;
    }

    long estimatedNanosToSend(String message) {
        return message.length();
    }
}

1.2 中断锁

如果要将显式锁应用到可以取消的任务重,就需要让获得锁的操作是支持中断。 lockInterruptibly方法可以应用到这样情况中,其不仅能获得锁,还能保持对于中断的响应。

public class InterruptibleLocking {
    private Lock lock = new ReentrantLock();

    public boolean sendOnSharedLine(String message)
            throws InterruptedException {
        // 可以响应中断的锁
        lock.lockInterruptibly();
        try {
            return cancellableSendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }

    // 可能会抛出中断异常
    private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
        /* send something */
        return true;
    }

}

1.3 非块结构的加锁

在内置锁中,锁的获得和锁的释放都是在同一块代码的,这样简洁清楚还便于使用,不用考虑如何退出代码块。但是加锁的位置不一定只有代码块,比如之前谈过的分段锁ConcurrentHashMap中利用了分段锁对散列表中的元素分段上锁,实现了并发访问容器元素的功能。如果是这种非块结构的加锁,就不能应用内置锁,而是需要使用显式锁控制。同样,链表类的容器可以应用分段锁,来支持并发访问不同链表元素。

2 性能因素考虑

前文中曾经提过,ConcurrentHashMap和同步的HashMap相比,其性能优势在于利用了分段锁对散列表中的元素分段上锁,故而支持并发访问容器中不同的元素。同理,和内置锁相比,显式锁都优势在于更好的性性。锁的实现方式越好,就越可以避免不必要的系统调用和上下文切换,以提高效率。

线程间的切换,涉及线程挂起和恢复等一系列操作,这样的线程上下文的切换很是消耗性能,所以要避免不必要的线程切换。

Java 6中对内置锁的进行了优化,现在内置锁和显式锁相比性能已经很接近,只略低一些。

3. 公平锁

ReentrantLock的构造函数中提供两种锁的类型:

  • 公平锁:线程将按照它们请求锁的顺序来获得锁;
  • 非公平锁:允许插队,如果一个线程请求非公平锁的那个时刻,锁的状态正好为可用,则该线程将跳过所有等待中的线程获得该锁。

非公平锁在线程间竞争锁资源激烈的情况下,性能更高,这是由于:在恢复一个被挂起线程与该线程真正开始运行之间,存在着一个很严重的延迟,这是由于线程间上下文切换带来的。正是这个延迟,造成了公平锁在使用中出现CPU空闲。非公平锁正是将这个延迟带来的时间差利用起来,优先让正在运行的线程获得锁,避免线程的上下文切换。

如果每个线程获得锁的时间都很长,或者请求锁的竞争很稀疏或不频繁,则公平锁更为适合。

内置锁和显式锁都是默认使用非公平锁,但是显式锁可以设置公平锁,内置锁无法做到。

4. 同步方法和显式锁的选择

显式锁虽然更为灵活,提供更为丰富的功能,且性能更好,但是还是推荐先使用同步(Synchronized)方法,这是因为同步方法的内置锁,使用起来更为方便,简洁紧凑 ,还便于理解,也更为开发人员所熟悉。

建议只有在一些内置锁无法满足的情况下,再将显式锁ReentrantLock作为高级工具使用,比如要使用轮询锁、定时锁、可中断锁或者是公平锁。除此之外,还应该优先使用synchronized方法。

5. 读-写锁

无论是显式锁还是内置锁,都是互斥锁,也就是同一时刻只能有一个线程得到锁。互斥锁是保守的加锁策略,可以避免“写-写”冲突、“写-读”冲突”和"读-读"冲突。但是有时候不需要这么严格 ,同时多个任务读取数据是被允许,这有助于提升效率,不需要避免“读-读”操作。为此,Java 5.0 中出现了读-写锁ReadWriteLock

ReadWriteLock可以提供两种锁:

  • 读锁readLock:允许多个线程同时执行读操作,但是同时只能有一个线程执行写操作;
  • 写锁writeLock:正常的互斥锁,同一时刻只能有一个线程执行读写操作。

ReentrantReadWriteLock是读写锁支持重入的实现,下面的例子中利用读写锁实现了支持并发读取元素的多线程安全Map:

public class ReadWriteMap <K,V> {
    private final Map<K, V> map;
    // 读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 读锁
    private final Lock r = lock.readLock();
    // 写锁
    private final Lock w = lock.writeLock();

    public ReadWriteMap(Map<K, V> map) {
        this.map = map;
    }

    public V put(K key, V value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }
    
    public V get(Object key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    .....
}

不过需要注意的是,虽然读写锁的出现是为了提高效率,但只适用于对多线程频繁并发执行读操作的情况。如果是在正常的情况下使用读写锁,反而会降低效率,因为ReadWriteLock需要额外的开销维护分别维护读锁和写锁,得不偿失。

扩展阅读:

  1. 多线程安全性:每个人都在谈,但是不是每个人都谈地清
  2. 对象共享:Java并发环境中的烦心事
  3. 从Java内存模型角度理解安全初始化
  4. 从任务到线程:Java结构化并发应用程序
  5. 关闭线程的正确方法:“优雅”的中断
  6. 驾驭Java线程池:定制与扩展
  7. 探秘Java并发模块:容器与工具类
相关文章
|
7天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
17 5
Java反射机制:解锁代码的无限可能
|
1天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
7 2
|
6天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
14 3
|
6天前
|
安全 Java UED
深入理解Java中的异常处理机制
【10月更文挑战第25天】在编程世界中,错误和意外是不可避免的。Java作为一种广泛使用的编程语言,其异常处理机制是确保程序健壮性和可靠性的关键。本文通过浅显易懂的语言和实际示例,引导读者了解Java异常处理的基本概念、分类以及如何有效地使用try-catch-finally语句来处理异常情况。我们将从一个简单的例子开始,逐步深入到异常处理的最佳实践,旨在帮助初学者和有经验的开发者更好地掌握这一重要技能。
14 2
|
8天前
|
Java 数据库连接 开发者
Java中的异常处理机制####
本文深入探讨了Java语言中异常处理的核心概念,通过实例解析了try-catch语句的工作原理,并讨论了finally块和throws关键字的使用场景。我们将了解如何在Java程序中有效地管理错误,提高代码的健壮性和可维护性。 ####
|
10天前
|
安全 Java 程序员
深入浅出Java中的异常处理机制
【10月更文挑战第20天】本文将带你一探Java的异常处理世界,通过浅显易懂的语言和生动的比喻,让你在轻松阅读中掌握Java异常处理的核心概念。我们将一起学习如何优雅地处理代码中不可预见的错误,确保程序的健壮性和稳定性。准备好了吗?让我们一起踏上这段旅程吧!
22 6
|
8天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 1
|
2天前
|
Java 开发者
深入理解Java异常处理机制
【10月更文挑战第29天】在Java的世界中,异常处理如同生活的调味品,不可或缺。它确保了程序在遇到错误时不会崩溃,而是优雅地继续运行或者给出提示。本文将带你领略异常处理的奥秘,从基础的try-catch语句到高级的自定义异常,让你在面对程序中的各种“意外”时,能够从容应对。
|
4天前
|
SQL Java
探索Java中的异常处理机制
【10月更文挑战第26天】 在本文中,我们将深入探讨Java编程语言的异常处理机制。通过分析不同类型的异常、异常的捕获与抛出方式,以及如何自定义异常类,读者将能够更好地理解并应用Java中的异常处理机制来提高代码的健壮性和可读性。
11 0
|
10天前
|
Java 开发者 UED
Java中的异常处理机制及其重要性
【10月更文挑战第20天】 在Java编程中,异常处理是确保程序健壮性的关键。本文将探讨Java中的异常处理机制,包括其定义、类型、抛出和捕获异常的方法,以及如何自定义异常。通过实例说明,我们将展示异常处理在实际编程中的应用,帮助读者理解其在提高代码质量和稳定性方面的重要性。
11 0