读写锁的作用
为什么要用读写锁
我们都知道,读写锁可以提高效率,但是怎么提高效率呢?
我们通常说到读写锁的时候就会说:读数据的时候用读锁,写数据的时候,用写锁,读锁是共享锁,也就是说,可以一起去读数据相互之间不影响,
和没上锁,好像也没什么区别。
写锁是排它锁,当一个线程进入到写锁之后,那么其它的线程,就都只能等待了。
上面说到读取数据的时候用读锁,好像和没上锁,没什么区别?真的没区别吗?答案肯定是有区别。
其实如果你弄多线程出来整个流程只是为了去读取数据,没有对你读的数据做写的操作,那还真是没必要去上什么锁,浪费代码。
模拟一个简单不需要上锁的场景:
场景《1》:现在有三个线程都是要去读取数据num,变量num的数据又不会变,你们三个线程想怎么读就怎么读,我才懒得管你,读出来的数据都是一样的,又没什么影响。(当然如果有需求,要按顺序读取的例外。)
那什么时候才去用读锁呢?
其实我们要用到读锁的时候,都是伴随着要用到写锁,也就是说读锁是和写锁打配合的。
现在我们把上面的场景变一下:
场景《2》:现在还是有三个线程,但是现在是有两个线程要去读数据num,而一个线程要去给num附上新值。
这种情况下:如果我们不加锁,就会出现数据的脏读。
解决脏读的方法有很多种,比如说synchronized方法,synchronized代码块,ReentrantLock,写锁,或者我们这里要说的的读写锁一起用等等。。。。。
我们就拿synchronized来和读写锁比较一下:
public class TestNum { public static void main(String[] args) { final Num num = new Num(); Runnable worker = new Runnable() { @Override public void run() { num.getnum(); } }; Runnable worker1 = new Runnable() { @Override public void run() { num.getnum(); } }; Runnable worker2 = new Runnable() { @Override public void run() { num.setnum(); } }; new Thread(worker,"第一个").start(); new Thread(worker1,"第二个").start(); new Thread(worker2,"第三个").start(); } } class Num{ private Integer num =0; //获取num public synchronized void getnum(){ System.out.println(Thread.currentThread().getName()+"读num的时候进来:"+num); } //设置num public synchronized void setnum(){ //设置num=1 num=1; System.out.println(Thread.currentThread().getName()+"写后的num:"+num); } }
运行会出现三种结果
第一种得到结果:
第一个读num的时候进来:0
第三个写后的num:1
第二个读num的时候进来:1
第二种得到结果:
第一个读num的时候进来:0
第二个读num的时候进来:0
第三个写后的num:1
第三种得到结果
第三个写后的num:1
第一个读num的时候进来:1
第二个读num的时候进来:1
结果数据很正常,但是我们会发现第一次的结果数据,很像是串行的,也更好说明一下问题。我们知道synchronized方法会使三个线程,不管你是读线程还是写线程,每次只能进去一个,也就是说,一个线程进入到了读数据getnum()里面去了
其它两个线程都是需要等这个线程完成操作释放锁,其它线程才能去读数据getnum()或者写数据setnum()。这样做就会不管你是读线程还是写线程都是串行的。串行的肯定效率低一点。
如何能提高效率?
我们可以想一下,如果两个读数据的线程,能并行去运行,一起去读数据getnum(),只让写数据的那个线程等待,这样我们的数据也不会发生脏读,而且效率也是会提高一点。
要完成这个操作,就可以用到我们的读写锁了
读写锁代码如下:
public class TestNum { public static void main(String[] args) { final Num num = new Num(); Runnable worker = new Runnable() { @Override public void run() { num.getnum(); } }; Runnable worker1 = new Runnable() { @Override public void run() { num.getnum(); } }; Runnable worker2 = new Runnable() { @Override public void run() { num.setnum(); } }; new Thread(worker,"第一个").start(); new Thread(worker1,"第二个").start(); new Thread(worker2,"第三个").start(); } } class Num{ private Integer num =0; ReentrantReadWriteLock readwrlock = new ReentrantReadWriteLock(); //获取num public void getnum(){ //读数据的时候用读锁 try { //加读锁 readwrlock.readLock().lock(); System.out.println(Thread.currentThread().getName()+"读num的时候进来:"+num); } catch (Exception e) { e.printStackTrace(); }finally{ //解锁 readwrlock.readLock().unlock(); } } //设置num public void setnum(){ //写数据的时候用写锁 try { //加写锁 readwrlock.writeLock().lock(); //设置num=1 num=1; System.out.println(Thread.currentThread().getName()+"写后的num:"+num); } catch (Exception e) { e.printStackTrace(); }finally{ //解锁 readwrlock.writeLock().unlock(); } } }
没有特殊情况的话《就是一个读锁,读完了,另一个读锁居然还没进来,被写锁拿到写锁了,这种情况,上面的代码应该是不会发生的》,运行结果一般,只会有两种:
第一种:
第一个读num的时候进来:0
第二个读num的时候进来:0
第三个写后的num:1
第二种
第三个写后的num:1
第二个读num的时候进来:1
第一个读num的时候进来:1
看着这两种结果,我们会发现,两个读操作一直都是在一起的。
这就是我们读写锁机制的妙用:
当一个线程得到读锁的时候,不允许其它线程得到写锁,但是可以让其它线程得到读锁,所以读操作之间就可以并行运行,相互之间不需要等待。
当一个线程得到写锁的时候,就会不允许其它线程得到写锁或者读锁,但是这个线程可以让自己在有写锁的情况下,获取到读锁,这个我们下篇再来讲这个机制,读写锁的锁降级的机制。