java ReentrantLock 设计的好处与缺点?与简单的synchronized关键字对比
ReentrantLock
是 Java 中用于实现锁机制的一种可重入锁。它具有一些优点和一些缺点,下面是关于ReentrantLock
的主要优点和缺点:
优点:
-
可重入性(Reentrancy):
ReentrantLock
支持锁的可重入性,也就是说同一个线程可以多次获得同一个锁,而不会导致死锁。这使得在复杂的程序中更容易实现正确的同步。 -
公平性(Fairness):
ReentrantLock
可以选择是否使用公平性策略。在公平模式下,等待锁的线程会按照请求的顺序获得锁,而不会出现饥饿现象。这有助于避免某些线程永远无法获取锁的问题。 -
灵活性(Flexibility):
ReentrantLock
提供了丰富的方法和选项,可以更灵活地控制锁的行为。例如,你可以设置锁的超时时间、中断等待锁的线程,以及尝试非阻塞地获取锁等。 -
性能(Performance):在某些情况下,
ReentrantLock
可以比内置的synchronized
关键字更好地支持高并发性能,因为它具有更细粒度的控制和更少的竞争。
缺点:
-
复杂性(Complexity):相对于简单的
synchronized
关键字,ReentrantLock
使用起来更复杂。需要手动管理锁的获取和释放,容易出现编程错误,如忘记释放锁或死锁。 -
资源占用(Resource Usage):
ReentrantLock
使用的资源比synchronized
多,因为每个锁都需要额外的对象来表示,这可能会导致内存占用增加。 -
性能变化(Performance Variability):虽然
ReentrantLock
在某些情况下可以提供更好的性能,但在其他情况下,它的性能可能不如synchronized
。性能取决于具体的应用场景和锁的使用方式。
总的来说,ReentrantLock
是一个强大且灵活的同步工具,它在某些情况下可以比synchronized
更有优势,但需要谨慎使用,并且需要仔细考虑如何使用以确保线程安全。在选择使用它时,你应该考虑你的应用需求、复杂性和性能要求。
在大多数情况下,ReentrantLock
和synchronized
关键字在性能上没有明显的差异,因为它们都可以用于实现互斥访问。性能的主要差异通常来自于如何使用它们,以及锁的粒度。
以下是一些比较ReentrantLock
和synchronized
的关键差异和考虑因素:
-
灵活性:
ReentrantLock
提供了更多的灵活性。你可以使用它来实现更复杂的同步需求,如可中断锁、定时锁、公平锁等。它还允许你在不同的代码块中获得和释放锁,这可以对性能进行微调。与之相比,synchronized
关键字较为简单,但功能相对受限。 -
锁粒度:性能的关键因素之一是锁的粒度。如果你在整个方法上使用
synchronized
,那么只有一个线程能够执行整个方法,这可能导致性能瓶颈。与之相比,ReentrantLock
允许你更细粒度地控制锁的范围,这可以减小锁的竞争,提高性能。你可以在需要同步的代码块上获取和释放锁,而不是整个方法。 -
易用性:
synchronized
相对于ReentrantLock
更容易使用,因为它是Java语言的一部分,无需导入额外的库,而且语法更简单。 -
分布式环境:如果你的应用需要在分布式环境中实现并发控制,
ReentrantLock
通常更适合,因为它可以很容易地扩展到分布式锁实现,如基于ZooKeeper或Redis的分布式锁。
总的来说,性能上的差异通常不大,但在具体情况下,你可能会发现一种方式比另一种方式更适合你的应用需求。要选择哪种方式,需要综合考虑你的应用场景、性能需求以及代码的复杂性。如果你的应用需求比较简单,而且性能不是主要关注点,那么synchronized
可能更方便。如果你需要更高级的同步功能或更细粒度的控制,那么ReentrantLock
可能更适合。
public class CodeService { private Lock lock1 = new ReentrantLock(); private Lock lock2 = new ReentrantLock(); private Lock lock3 = new ReentrantLock(); public DResult updateSNO() { DResult dr = new DResult(); try { lock1.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock1.unlock(); // 释放锁 } return dr; } public DResult updateSNO1() { DResult dr = new DResult(); try { lock2.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock2.unlock(); // 释放锁 } return dr; } public DResult updateSNO2() { DResult dr = new DResult(); try { lock3.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock3.unlock(); // 释放锁 } return dr; } }
这种方式允许每个方法在自己的独立锁上执行,不会相互阻塞,从而实现了并发执行。
以下方式同一线程只能执行一个 updateSNO
方法。这是因为你使用了一个共享的 ReentrantLock
锁实例 lock
来同步这三个方法,当一个线程获取了锁执行其中一个方法时,其他线程必须等待该线程释放锁才能执行其中任何一个方法。
如果你希望同一线程可以并发地执行这些方法,你可以为每个方法创建不同的锁实例,这样每个方法都有自己的独立锁,不会相互阻塞。应该用前一种方式
public class CodeService { private Lock lock= new ReentrantLock(); public DResult updateSNO() { DResult dr = new DResult(); try { lock1.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock1.unlock(); // 释放锁 } return dr; } public DResult updateSNO1() { DResult dr = new DResult(); try { lock.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock.unlock(); // 释放锁 } return dr; } public DResult updateSNO2() { DResult dr = new DResult(); try { lock.lock(); // 获取锁 // 具体更新逻辑 } catch (Exception e) { // 异常处理 } finally { lock.unlock(); // 释放锁 } return dr; } }
此时与如下的代码效果差不多了
public class CodeService { // 定义一个虚拟的锁对象 private final Object codeDAOLock = new Object(); public DResult updateSNO() { DResult dr = new DResult(); try { // 锁定 codeDAOLock 对象,只锁定 codeDAO.updateSNO 方法的调用 synchronized (codeDAOLock) { .... } } catch (Exception e) { dr.setContent("失败:出现意外错误," + e.toString()); dr.setStatusCode(0); } return dr; } }