JUC学习笔记 -- (9) ReadWriteLock读写锁的使用

  说到Java并发编程,很多开发第一个想到同时也是经常常用的肯定是Synchronized,但是Synchronized存在明显的一个性能问题就是读与读之间互斥,简言之就是,我们编程想要实现的最好效果是,可以做到读和读互不影响,读和写互斥,写和写互斥,提高读写的效率,这就要用到本章的重点ReadWriteLock读写锁。

之前文章已经介绍了Lock的一种实现ReentrantLock的使用,其在功能上和Synchronized相同,只不过这个更具灵活性。而本章介绍的ReadWriteLock也是Lock的一种实现,而且相对于ReentrantLock和Synchronized,ReadWriteLock在性能上更具优势。

Java并发包中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();
}

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
在具体讲解ReetrantReadWriteLock的使用方法前,我们有必要先对其几个特性进行一些深入学习了解。

1. ReetrantReadWriteLock特性说明

1.1 获取锁顺序

  • 非公平模式(默认)
    当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
  • 公平模式
    当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

1.2 可重入

什么是可重入锁,不可重入锁呢?"重入"字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的,这里通过一个例子着重说下可重入锁的释放需要的事儿。

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                System.out.println("Thread real execute");
                lock.writeLock().unlock();
            }
        });

        lock.writeLock().lock();
        lock.writeLock().lock();
        t.start();
        Thread.sleep(200);
        
        System.out.println("realse one once");
        lock.writeLock().unlock();
    }

}

从运行结果中,可以看到,程序并未执行线程的run方法,由此我们可知,上面的代码会出现死锁,因为主线程2次获取了锁,但是却只释放1次锁,导致线程t永远也不能获取锁。一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开synchornized方法(代码块),就是一次完整的锁获取和释放。

JUC学习笔记 -- (9) ReadWriteLock读写锁的使用_第1张图片

 

1.3 锁降级

要实现一个读写锁,需要考虑很多细节,其中之一就是锁升级和锁降级的问题。什么是升级和降级呢?ReadWriteLock的javadoc有一段话:

Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock, in preference to other waiting readers or writers?

翻译过来的结果是:在不允许中间写入的情况下,写入锁可以降级为读锁吗?读锁是否可以升级为写锁,优先于其他等待的读取或写入操作?简言之就是说,锁降级:从写锁变成读锁;锁升级:从读锁变成写锁,ReadWriteLock是否支持呢?让我们带着疑问,进行一些Demo 测试代码验证。

锁升级demo:

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
        lock.readLock().lock();
        System.out.println("读数据...");
        
        lock.writeLock().lock();
        System.out.println("写数据...");
    }
}

执行结果:

 

结论:上面的测试代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的

锁降级demo:

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
        
        lock.writeLock().lock();
        System.out.println("写数据...");
        
        lock.readLock().lock();
        System.out.println("读数据...");
    }
}

执行结果:

结论:ReentrantReadWriteLock支持锁降级,上面代码不会产生死锁。这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

 

2.ReetrantReadWriteLock和syncronized对比使用

2.1 Synchronized实现

在使用ReetrantReadWriteLock实现锁机制前,我们先看一下,多线程同时读取文件时,用synchronized实现的效果

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
    	for(int i=0;i<2;i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					get();
				}
			},"读线程"+i).start();
		}
    }
    
    public synchronized static void get(){
		for(int i=0;i<5;i++){
			try {
				Thread.currentThread().sleep(20);
			} catch (Exception e) {
			}
			System.out.println(Thread.currentThread().getName()+"正在读数据-->"+i);
		}
		System.out.println(Thread.currentThread().getName()+"读取数据完成!");
    }
}

执行结果:

JUC学习笔记 -- (9) ReadWriteLock读写锁的使用_第2张图片

从运行结果可以看出,两个线程的读操作是顺序执行的.

2.2 ReetrantReadWriteLock实现

public class TestReadWriteLock {

	public static void main(String[] args) {
		final ReadWriteLockDemo demo = new ReadWriteLockDemo();

		for(int i=0;i<2;i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					demo.get();
				}
			},"读线程"+i).start();
		}
	}

}

class ReadWriteLockDemo{

	private ReadWriteLock lock = new ReentrantReadWriteLock();

	public void get(){
		lock.readLock().lock();
		try{
			for(int i=0;i<5;i++){
				try {
					Thread.currentThread().sleep(20);
				} catch (Exception e) {
				}
				System.out.println(Thread.currentThread().getName()+"正在读数据-->"+i);
			}
			System.out.println(Thread.currentThread().getName()+"读取数据完成!");
		}finally{
			lock.readLock().unlock();
		}
	}
}

执行结果:

JUC学习笔记 -- (9) ReadWriteLock读写锁的使用_第3张图片

从运行结果可以看出,两个线程的读操作是同时执行的。
 

通过上面的测试代码,我们也可以延伸得出一个结论,ReetrantReadWriteLock读锁使用共享模式,即:同时可以有多个线程并发地读数据。但是另一个问题来了,写锁之间是共享模式还是互斥模式?读写锁之间是共享模式还是互斥模式呢?下面让我们通过Demo进行一一验证吧。

3.1 ReetrantReadWriteLock读写锁关系

public class TestReadWriteLock {

	public static void main(String[] args) {
		final ReadWriteLockDemo demo = new ReadWriteLockDemo();

		new Thread(new Runnable() {
			@Override
			public void run() {
				demo.get();
			}
		},"读线程").start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				demo.set();
			}
		},"写线程").start();
	}

}

class ReadWriteLockDemo{

	private ReadWriteLock lock = new ReentrantReadWriteLock();

	public void get(){
		lock.readLock().lock();
		try{
			for(int i=0;i<5;i++){
				try {
					Thread.currentThread().sleep(20);
				} catch (Exception e) {
				}
				System.out.println(Thread.currentThread().getName()+"正在读数据-->"+i);
			}
			System.out.println(Thread.currentThread().getName()+"读取数据完成!");
		}finally{
			System.out.println("释放读锁!");
			lock.readLock().unlock();
		}
	}

	public void set(){
		lock.writeLock().lock();
		try{
			for(int i=0;i<5;i++){
				try {
					Thread.currentThread().sleep(20);
				} catch (Exception e) {
				}
				System.out.println(Thread.currentThread().getName()+"正在写数据-->"+i);
			}
			System.out.println(Thread.currentThread().getName()+"写数据完成!");
		}finally{
			System.out.println("释放写锁!");
			lock.writeLock().unlock();
		}
	}
}

执行结果:

JUC学习笔记 -- (9) ReadWriteLock读写锁的使用_第4张图片

结论:读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥。

3.2 ReetrantReadWriteLock写锁关系

public class TestReadWriteLock {

	public static void main(String[] args) {
		final ReadWriteLockDemo demo = new ReadWriteLockDemo();

		new Thread(new Runnable() {
			@Override
			public void run() {
				demo.set();
			}
		},"写线程1").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				demo.set();
			}
		},"写线程2").start();
	}

}

class ReadWriteLockDemo{

	private ReadWriteLock lock = new ReentrantReadWriteLock();

	public void set(){
		lock.writeLock().lock();
		try{
			for(int i=0;i<5;i++){
				try {
					Thread.currentThread().sleep(20);
				} catch (Exception e) {
				}
				System.out.println(Thread.currentThread().getName()+"正在写数据-->"+i);
			}
			System.out.println(Thread.currentThread().getName()+"写数据完成!");
		}finally{
			System.out.println("释放写锁!");
			lock.writeLock().unlock();
		}
	}
}

执行结果:

JUC学习笔记 -- (9) ReadWriteLock读写锁的使用_第5张图片

4. 总结

1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(JUC)