并发读写的时候,很容易造成数据不一致的状态
上案例,代码如下:
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 0; i < 5; i++) { final int finali= i; new Thread(() -> { try { myCache.put(finali+"", finali+""); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } for (int i = 0; i < 5; i++) { final int finali= i; new Thread(() -> { try { myCache.get(finali+""); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } } class MyCache { private volatile Map<String, Object> map = new HashMap<>(); public void put(String key, Object value) throws InterruptedException { System.out.println(Thread.currentThread().getName() + "\t-----写入数据key"); Thread.sleep(3000); map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t-----写入数据成功"); } public void get(String key) throws InterruptedException { System.out.println(Thread.currentThread().getName() + "\t读取数据key"); Thread.sleep(3000); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t读取数据成功" + result); } }
运行结果如下:
我们可以看到的是在1进行写入数据的时候,此时还没有写入成功,就已经对1进行了读取操作,就像我们数据库的原子性一样,这里在还没有对数据进行写入完成就进行了读取的操作,所以读取的为空。
接下来我们看看加入了读写锁的效果,这里只需要对MyCache进行修改:
加入ReadWriteLock
ReadWriteLock的作用:保证并发读
- 写锁是独占锁,所谓独占即为独自占有,别的线程既不能获取到该锁的写锁,也不能获取到对应的读锁。
- 读锁是共享锁,所谓共享即是所有线程都可以共同持有该读锁
- 归纳与一句换:读读共存 读写不共存 写写不共存
代码如下 :
class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key, Object value) throws InterruptedException { readWriteLock.writeLock().lock(); //上写锁 try { System.out.println(Thread.currentThread().getName() + "\t-----写入数据key"); Thread.sleep(3000); map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t-----写入数据成功"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } public void get(String key) throws InterruptedException { readWriteLock.readLock().lock(); //上读锁 try { System.out.println(Thread.currentThread().getName() + "\t读取数据key"); Thread.sleep(3000); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t读取数据成功" + result); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } }
运行结果如下:
可以看到我们对写保持了一致性,读保证了可并发读,防止了在写的时候进行了读的操作导致的不一致性,所以这就是读写锁的作用
ReadWriteLock锁降级
锁降级过程(当前线程):
为什么可以降级? 为什么在写锁释放之前可以拿到读锁?
首先写锁是独占的,读锁是共享的,然后读写锁是线程间互斥的,锁降级的前提是所有线程都希望对数据变化敏感,但是因为写锁只有一个,所以会发生降级。
你既然拿到写锁了,其他线程就没法拿到读锁或者写锁了,你再拿读锁,其实不会和其他线程的写锁发送冲突的,因为你拿到写锁到写锁释放这段时间其他线程是无法拿到任何锁的。
注意以下情况不是锁降级
如果先释放写锁,再获取读锁,可能在获取之前,会有其他线程获取到写锁,阻塞读锁的获取,就无法感知数据变化了。所以需要先hold住写锁,保证数据无变化,获取读锁,然后再释放写锁。
其他线程在该线程释放写锁之前,写操作所做的数据更新对其他线程是不可见的。但是一旦写锁释放,数据更新操作就会对其他线程可见。
思考?
即使是先释放写锁,然后获取读锁可能也没有问题,只不过会可能会被其他线程的写锁阻塞一段时间;
但是并不意味着,随后的这个读操作看不到之前别的线程的写锁下的写操作,只要写锁被释放数据更新还是可以看到的,所以说,上述这句话“阻塞读锁的获取,那么当前线程无法感知线程T的数据更新” 感觉有些瑕疵
但是需要明确的一点:用锁降级的前提是读优先于写。
比如常见的查询系统,需要保证数据的随时可读,如果当新线程请求读锁的时候,当前持有写锁的线程需要马上进行降级,保证所有读锁的顺利获取,阻塞后续写锁。