Collin Nam


深入理解Java读写锁ReadWriteLock

Frank 2019-04-26 1048浏览 1条评论
首页/ 正文
分享到: / / / /

​ReentrantLock实现了一种标准的互斥锁,每次最多只有一个线程能持有ReentrantLock。但对于维护数据的完整性来说,互斥通常是一种过于强硬的加锁规则,因此也就不必要地限制了并发性。互斥是一种保守的加锁策略,虽然可以避免写写冲突和写读冲突,但同样也避免了读读冲突。在许多情况下,数据结构上的操作都是读操作,虽然他们也是可变的并且在某些情况下修改,但其中大多数访问操作都是读操作。此时,如果能够放宽加锁需求,允许多个执行读操作的线程同时访问数据结构,那么将提升程序的性能。

 

我们看一下ReadWriteLock接口的源码,里面有两个方法,一个用户读操作,一个用于写操作。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

在读-写锁实现的加锁策略中,允许多个操作同时进行,但每次只允许一个写操作。与Lock一样,ReadWriteLock可以采用多种不同的实现方式,这些方式在性能、调度保证、获取优先性、公平性以及加锁语义等方面有所不同。

 

ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义,与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁,直到写线程使用完并且释放了写入锁。在非公平锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程啊可以的,但从读线程升级为写线程则是不可以的,这样会导致死锁。

我们首先看一下内置锁,在高并发多线程环境下读取文件。

用内置锁synchronized实现,代码如下:

package com.cocurrent.demo;

/**
 * @Author: nanJunYu
 * @Description:
 * @Date: Create in  2018/9/18 16:11
 */
public class ReadWriteLockSyn {
    public synchronized void get(Thread thread) {
        long startTime = System.currentTimeMillis();
        System.out.println("startTime " + startTime);
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在执行读操作。。。。");
        }
        System.out.println(thread.getName() + ": 读操作执行完毕!");
        long endTime = System.currentTimeMillis();
        long exp = endTime - startTime;
        System.out.println("消耗了:" + exp);

    }

    public static void main(String[] args) {
        final ReadWriteLockSyn lock = new ReadWriteLockSyn();
        new Thread(new Runnable() {
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}

运行结果如下,差不多需要耗时120多毫秒左右

可以看到在使用synchronized内置锁进行控制读操作也是互斥的,必须要等待线程1执行完,线程2才能获得读锁完成工作,这样其实是效率很低的。

 

下面我们使用ReentrantReadWriteLock可重入🔐,来代替上面的操作,看看消耗时间,代码如下:

package com.cocurrent.demo;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: nanJunYu
 * @Description:
 * @Date: Create in  2018/9/18 16:34
 */
public class ReadWriteLock {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void get(Thread thread) {
        lock.readLock().lock();
        try {
            long startTime = System.currentTimeMillis();
            System.out.println("startTime:" + startTime);
            for (int i = 0; i < 5; i++) {
                Thread.sleep(20);
                System.out.println("正在进行读操作.......");
            }

            System.out.println(thread.getName() + ":读操作完毕");
            long endTime = System.currentTimeMillis();
            long exp = endTime - startTime;
            System.out.println("消耗了:" + exp);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLock lock = new ReadWriteLock();
        new Thread(new Runnable() {
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}
执行结果如下,平均112毫秒左右,比用synchronized快了一些

通过两次实验的对比,我们可以看出来,读写锁的效率明显高于synchronized关键字

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。读锁和写锁是互斥的。

下面我看一下 读写互斥体现在哪里,代码如下

package com.cocurrent.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author: Frank
 * @Description:
 * @Date: Create in  2018/9/18 16:45
 */
public class ReadWriteLockB {

    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * @Author: Frank
     * @Description:读文件的方法
     * @Date: Create in  2018/9/18 16:46
     * @params:
     * @return:
     */
    public void readText(Thread thread) {
        //获得读锁
        lock.readLock().lock();
        if (!lock.isWriteLocked()) {
            System.out.println("当前为读🔐!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                Thread.sleep(20);
                System.out.println(thread.getName() + ":正在进行读操作");
            }
            System.out.println(thread.getName() + ":读操作执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读🔐
            System.out.println("释放读🔐");
            lock.readLock().unlock();
        }
    }


    /**
     * @Author: Frank
     * @Description:写文件的方法
     * @Date: Create in  2018/9/18 16:47
     * @params:
     * @return:
     */
    public void writeText(Thread thread) {
        //获得写🔐
        lock.writeLock().lock();
        if (lock.isWriteLocked()) {
            System.out.println("当前为写🔐");
        }
        try {
            for (int i = 0; i < 5; i++) {
                Thread.sleep(20);
                System.out.println(thread.getName() + ": 正在进行写操作.......");
            }
            System.out.println(thread.getName() + ":写操作执行结束!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("释放写🔐");
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorRead = Executors.newCachedThreadPool();
        final ReadWriteLockB readWriteLockB = new ReadWriteLockB();
        executorRead.execute(new Runnable() {
            public void run() {
                readWriteLockB.readText(Thread.currentThread());
            }
        });

        executorRead.execute(new Runnable() {
            public void run() {
                readWriteLockB.readText(Thread.currentThread());
            }
        });

        ExecutorService executorWrite = Executors.newCachedThreadPool();
        executorWrite.execute(new Runnable() {
            public void run() {
                readWriteLockB.writeText(Thread.currentThread());
            }
        });

        executorRead.execute(new Runnable() {
            public void run() {
                readWriteLockB.readText(Thread.currentThread());
            }
        });
    }


}

 

 

可以看到上面代码读读操作穿插执行,写操作独立运行,我们总结下为什么:

a. 如果当前全局处于无锁状态,则当前线程获取读锁。

b. 如果当前全局处于读锁状态,且队列中没有等待线程,则当前线程获取读锁。

c. 如果当前全局处于写锁占用状态(并且不是当前线程占有),则当前线程入队尾。

d. 如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会。

e. 如果当前全局处于读锁状态,且等待队列中第一个等待线程不是写锁,则当前线程可以抢占读锁。

获取写锁相对就比较简单了,规则如下:

h. 如果当前处于无锁状态,则当前线程获取写锁。

i. 如果当前全局处于读锁状态,当前线程入队尾。

j. 如果当前全局处于写锁状态,除非是重入获取写锁,否则入队尾。

 

 

 原创公众号

     关注java 设计模式,JVM特性,

    并发编程、分布式、微服务,

   linux高可用集群,等相关技术。

        扫一扫关注我吧! 😀😀😀

     

 

 

最后修改:2019-04-26 17:56:37 © 著作权归作者所有
如果觉得我的文章对你有用,请随意赞赏
扫一扫支付

上一篇

发表评论

说点什么吧~

评论列表

Sun 2019-07-10 16:14:54
你好
回复