什么是读写锁
多个线程同时读一个资源类是没有任何问题的,所以在并发的情况下,读取共享资源应该是可以同时进行的;但是,如果一个线程想要去写共享资源,就不应该再有其他线程可以对该共享资源进行读或者是写操作了。
前面提到的ReentrantLock就保证了只有一个线程可以执行临界区代码。但是有些时候,这种保护有点过头。因为我们发现,任何时刻,只要进入临界区,就必须获取锁,那么别的线程也就访问不到了。但是,我们有些时候只想读取数据,不修改数据,如此一来,就会消耗时间。
实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。
读 | 写 | |
---|---|---|
读 | 可以 | 不可以 |
写 | 不可以 | 不可以 |
读写锁就是这个原理,即读写锁在同一时刻可以允许多个多线程访问,但是在写线程访问的时候,所有的读线程和其他写线程都会被阻塞。读写锁实际维护了一对锁,一个读锁,一个写锁,通过分离读锁和写锁,使得其并发性比独占式锁(排他锁)有了很大的提升。
示例
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Book book = new Book();//创建一本书,可读可写
//两个写手
for (int i = 0; i < 2; i++) {
int num = i;
new Thread(()->{
try {
readWriteLock.writeLock().lock();
System.out.println("写手"+num+"在写文章");
book.write(String.valueOf(num),UUID.randomUUID().toString().substring(0,5));
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
},String.valueOf(i)).start();
}
//6个读者
for (int i = 0; i < 6; i++) {
int num = i;
new Thread(()->{
try {
readWriteLock.readLock().lock();
String word = book.read(String.valueOf(num % 2));
System.out.println("读者"+num+"在阅读文章..."+word);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
},String.valueOf(i)).start();
}
}
}
class Book{
HashMap<String, String> map = new HashMap<>();
public void write(String key,String val){
map.put(key, val);
}
public String read(String key){
String word = map.get(key);
return word;
}
}
"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:F:\MyDir\IDEA\IDEA2018\IntelliJ IDEA 2018.2.4\lib\idea_rt.jar=54141:F:\MyDir\IDEA\IDEA2018\IntelliJ IDEA 2018.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;E:\idea_workplace\javaBase\target\classes"
写手0在写文章
写手1在写文章
读者0在阅读文章...6b021
读者1在阅读文章...220e4
读者2在阅读文章...6b021
读者4在阅读文章...6b021
读者3在阅读文章...220e4
读者5在阅读文章...220e4
Process finished with exit code 0
实现原理
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
//读锁
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
}
//写锁
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
}
//AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
}
}
读写锁的效率
读写锁是否会提高使用互斥锁的性能取决于数据被读取的频率与被修改的频率,读取和写入操作的持续时间以及数据的争用 ,尝试同时读取或写入数据的线程数。 例如,最初填充数据的集合,然后经常被修改的频繁的搜索(例如某种目录)是使用读写锁的理想候选。 然而,如果更新变得频繁,那么数据的大部分时间将被专门锁定,并且并发性增加很少。 此外,如果读取操作太短,则读写锁定实现(其本身比互斥锁更复杂)的开销可以支配执行成本,特别是因为许多读写锁定实现仍将序列化所有线程通过小部分代码。 最终,只有剖析和测量将确定使用读写锁是否适合您的应用程序。