上篇文章简单的介绍了ReentrantLock可重入锁。事实上我们可以理解可重入锁是一种排他锁,排他锁在同一个时刻只能够由一个线程进行访问。这就与我们实际使用过程中有点不想符合了,比如说当我们进行读写文件操作的时候,我们可能允许多个线程进行读文件操作,而对写文件只需要控制一个线程既可以。在这种业务情况下如果使用排他锁,可能不太符合而且效率也可能有些低下。
一、ReentrantReadWriteLock介绍
读写锁维护了一对锁,一个读锁和一个写锁。通过分离读锁和写锁使得并发性相比一般的排他锁在性能上有更好的一些优势。
二、ReentrantReadWriteLock的特性
公平性选择:支持公平锁和非公平锁。
可重入性:支持可重入性。例读线程获取了读锁以后可以继续获取读锁。写线程获取写锁以后可以继续获取写锁。
锁降级:遵循获取读锁,获取写锁在释放写锁的次序。支持写锁降级为读锁。
三、接口和API
读写锁ReentrantReadWriteLock实现接口ReadWriteLock。接口ReadWriteLock实现了两个方法。
即readLock()方法和writeLock()方法。
1 public interface ReadWriteLock { 2 Lock readLock(); 3 Lock writeLock(); 4 }
ReentrantReadWriteLock定义如下:
1 /** 内部类 读锁 */ 2 private final ReentrantReadWriteLock.ReadLock readerLock; 3 /** 内部类 写锁 */ 4 private final ReentrantReadWriteLock.WriteLock writerLock; 5 final Sync sync; 6 /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */ 7 public ReentrantReadWriteLock() { 8 this(false); 9 } 10 /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */ 11 public ReentrantReadWriteLock(boolean fair) { 12 sync = fair ? new FairSync() : new NonfairSync(); 13 readerLock = new ReadLock(this); 14 writerLock = new WriteLock(this); 15 } 16 /** 返回用于写入操作的锁 */ 17 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } 18 /** 返回用于读取操作的锁 */ 19 public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } 20 abstract static class Sync extends AbstractQueuedSynchronizer { 21 22 } 23 public static class WriteLock implements Lock, java.io.Serializable{ 24 25 } 26 public static class ReadLock implements Lock, java.io.Serializable { 27 28 }
举例读写锁使用。使用读写锁将线程不安全的HashMap集合缓存变为线程安全的。
1 public class Cache { 2 static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); 3 static Lock w = rwl.writeLock(); 4 // 获取一个key对应的value 5 public static final Object get(String key) { 6 r.lock(); try { 7 return map.get(key); 8 } finally { 9 r.unlock(); 10 } 11 } 12 // 设置key对应的value,并返回旧的value 13 public static final Object put(String key, Object value) { 14 w.lock(); try { 15 return map.put(key, value); 16 } finally { 17 w.unlock(); 18 } 19 } 20 21 // 清空所有的内容 22 public static final void clear() { 23 w.lock(); try { 24 map.clear(); 25 } finally { 26 w.unlock(); 27 } 28 } 29 }
四、读写锁的实现简单分析
在ReentrantLock中使用一个int类型的state来表示同步状态,该值表示锁被一个线程重复获取的次数。但是读写锁ReentrantReadWriteLock内部维护着一对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,高16为表示读,低16为表示写。
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。那么读写锁是如何迅速确定读锁和写锁的状态呢?通过为运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。
4.1、写锁的获取。写锁是一个可支持重入的排他锁。写锁的获取最终会调用tryAcquire(int arg)。注意:如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当 前写线程的操作。
源码中该方法为以下代码:
1 protected final boolean tryAcquire(int acquires) { 2 Thread current = Thread.currentThread(); 3 //当前锁个数 4 int c = getState(); 5 //写锁 6 int w = exclusiveCount(c); 7 if (c != 0) { 8 //c != 0 && w == 0 表示存在读锁 9 //当前线程不是已经获取写锁的线程 10 if (w == 0 || current != getExclusiveOwnerThread()) 11 return false; 12 //超出最大范围 13 if (w + exclusiveCount(acquires) > MAX_COUNT) 14 throw new Error("Maximum lock count exceeded"); 15 setState(c + acquires); 16 return true; 17 } 18 //是否需要阻塞 19 if (writerShouldBlock() || 20 !compareAndSetState(c, c + acquires)) 21 return false; 22 //设置获取锁的线程为当前线程 23 setExclusiveOwnerThread(current); 24 return true; 25 }
4.2、写锁的释放:当写锁中写锁的状态值为0的时候。从而等待的线程这个时候可以获取读写锁了。
4.3、读锁的获取:读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态(依靠CAS保证线程安全)。如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。
4.4、读锁的释放:读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
五、锁降级
锁降级指的是写锁降级为读锁的情况。详细具体指的是当前拥有写锁的线程,在获取到读锁,然后在释放写锁的过程。注意:如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。
1 public void processData() { 2 readLock.lock(); 3 if (!update) { 4 // 必须先释放读锁 5 readLock.unlock(); 6 // 锁降级从写锁获取到开始 7 writeLock.lock(); 8 try { 9 if (!update) { 10 // 准备数据的流程(略) 11 update = true; 12 } 13 readLock.lock(); 14 } finally { 15 writeLock.unlock(); 17 } 18 try { 19 20 } 21 // 锁降级完成,写锁降级为读锁 22 // 使用数据的流程(略) 23 } finally { 24 readLock.unlock(); 25 } 26 }
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前A线程不获取读锁而是直接释放写锁,假设此刻另一个线程B获取了写锁并修改了数据,那么当前A线程无法感知线程B的数据更新。如果当前线程A获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的。
参考:
1、Java并发编程的艺术
2、Java并发编程网