目录
显式地同步功能——锁
Java SE 5之后,并发包中新增 了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。
一个简单的Lock使用方式的示例:
// ReentrantLock实现简单的独占锁 public class LockDemo { private Lock lock = new ReentrantLock(); private int count = 0; public void doSth () { lock.lock(); try { Thread.sleep(1); count++; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { final LockDemo lockDemo = new LockDemo(); for (int i = 0; i < 1000; i++) { new Thread(()->{lockDemo.doSth();}).start(); } Thread.sleep(3000); System.out.println("result:"+lockDemo.count); } }
ReentrantLock是独占锁,就是能够防止多个线程同时访问共享资源的锁。当然,也有共享锁,比如ReadWriteLock,这种读写锁的实现允许多个线程同时读取数据,但只允许一个线程写入数据。这在读多写少的场景中可以提高性能和吞吐量。
public class RwLockDemo { private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private Map<String, Integer> cacheMap = new HashMap<>(); private Lock read = readWriteLock.readLock(); private Lock write = readWriteLock.writeLock(); public Object get(String key) { System.out.println("start read ..."); read.lock(); try { return cacheMap.get(key); } finally { read.unlock(); } } public Object put(String key,Integer value){ System.out.println("start write ..."); write.lock(); try { return cacheMap.put(key, value); } finally { write.unlock(); } } public static void main(String[] args) throws InterruptedException { final RwLockDemo rwLockDemo = new RwLockDemo(); rwLockDemo.put("a",1); rwLockDemo.put("b",2); System.out.println(rwLockDemo.cacheMap.get("a")); } }
synchronized和ReentrantLock的区别
-
锁的实现:synchronized是java语言的关键字,基于JVM实现。ReentrantLock基于JDK的API实现的(lock()和unlock()配合finally语句块实现的。)
-
性能:JDK1.6以前,synchronized重量级锁性能略差;1.6以后加入自适应自旋、锁消除等,两者性能就差不多了。
-
功能特点:ReentrantLock提供了些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
区别 | synchronized | ReentrantLock |
---|---|---|
锁实现机制 | 对象头监视器模式 | 依赖AQS |
灵活性 | 不灵活 | 支持响应中断、超时、尝试获取锁 |
释放锁形式 | 自动释放锁 | 显式调用unlock() |
支持锁类型 | 非公平锁 | 公平锁和非公平锁 |
条件队列 | 单条件队列 | 多条件队列 |
可重入支持 | 支持 | 支持 |
ReentrantLock实现原理
ReentrantLock
的一些关键实现原理:
-
CAS操作:
ReentrantLock
的实现依赖于CAS(Compare-And-Swap)操作,这是一种原子操作,用于在多线程环境中修改共享变量。CAS操作允许一个线程比较内存中的值与预期值,如果相等,则更新为新的值。 -
锁状态:
ReentrantLock
内部维护了一个锁状态(lock state),这个状态可以告诉锁当前是被哪个线程占用的,以及该线程占用了几次锁。这允许同一个线程多次获取同一把锁而不会发生死锁。当一个线程首次获取锁时,锁状态设置为1,每次再次获取锁时,状态递增。 -
AQS(AbstractQueuedSynchronizer):
ReentrantLock
的实现基于AQS,这是一个用于构建锁和其他同步器的抽象框架。AQS提供了队列、状态管理、等待线程管理等功能。ReentrantLock
通过扩展AQS并根据自身需求实现其中的一些方法来实现锁的功能。 -
公平性和非公平性:
ReentrantLock
支持公平性和非公平性。在公平模式下,等待线程按照FIFO顺序获取锁。在非公平模式下,等待线程有可能插队,当锁释放时,可能不是等待时间最长的线程获得锁。这个选择可以通过ReentrantLock
的构造函数来指定。 -
可中断性:
ReentrantLock
允许等待锁的线程响应中断信号,通过使用lockInterruptibly()
方法可以实现这一点。 -
条件变量:
ReentrantLock
还提供了条件变量(Condition
),允许线程在某些条件下等待和唤醒。条件变量是ReentrantLock
的一部分,可以通过newCondition()
方法创建。
ReentrantLock公平锁和非公平锁的差异
ReentrantLock
可以用于实现公平锁和非公平锁,它们的主要区别在于线程获取锁的顺序和策略。
-
公平锁(Fair Lock):
-
公平锁确保线程按照请求锁的顺序获得锁资源,即按照FIFO(先进先出)的顺序分配锁。
-
当一个线程请求锁但锁已经被其他线程占用时,该线程会进入等待队列,并按照请求锁的顺序排队等待。
-
当锁释放时,等待队列中的线程中最早请求锁的线程将获得锁,以确保公平性。
-
优点:公平锁能够避免线程饥饿,即某个线程无限期地等待获取锁的情况。每个线程最终都有机会获取锁。
-
缺点:因为需要维护等待队列和按照顺序分配锁,公平锁的性能可能相对较差,特别是在高并发情况下。
-
使用方式:通过在创建
ReentrantLock
实例时传递true
参数来创建公平锁,例如:ReentrantLock fairLock = new ReentrantLock(true);
-
-
非公平锁(Non-Fair Lock):
-
非公平锁不考虑线程请求锁的顺序,当锁可用时,任何等待的线程都有机会立即获得锁。
-
当锁被释放时,如果有等待的线程,JVM会从等待线程中随机选择一个线程来获得锁。
-
优点:非公平锁的性能通常比公平锁更好,因为它减少了维护等待队列和按照顺序分配锁的开销。
-
缺点:非公平锁可能导致某些线程一直无法获取锁,因为它们不会按照请求锁的顺序获得锁,可能会导致线程饥饿的情况。
-
使用方式:默认情况下,
ReentrantLock
创建的是非公平锁,可以不传递参数或传递false
来明确使用非公平锁。
-
选择公平锁还是非公平锁取决于应用的需求和性能考虑。如果你需要确保线程以公平的方式获得锁资源,可以使用公平锁。如果性能更为重要,而且你不担心某些线程可能被饿死,那么非公平锁可能是更好的选择。一般来说,非公平锁在高并发场景下表现更好,但可能会导致某些线程长时间等待。
ReentrantLock时序图
-
入口:ReentrantLock.java中的lock()方法调用了继承自AbstractQueuedSynchronizer的抽象静态类Sync的lock()方法,默认情况下创建的是非公平锁。(部分源码)
private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer {} public void lock() { sync.lock(); }
-
获取同步状态:非公平锁直接CAS尝试抢占,如果锁没有被抢占,直接抢占成功;大部分时候是抢占失败的,调用acquire()独占式获取同步状态会调用继承自AQS进行重写的acquire()判断tryAcquire()和addWaiter。
public final void acquire(int arg) if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
判断结果,同步失败,线程进入同步队列挂起状态等待
独占式同步状态获取流程
就是acquire(int arg)方法调用流程。如下所示: