前言
java中的锁大体可为分两种,一种叫排它锁,一种叫共享锁。排它锁,任意时刻只能有且只有一个线程持有,其它获取不到排它锁的线程要么自旋等待要么阻塞等待被唤醒。其中经常被我们提到的synchronized就是典型的排它锁,除此之外还有一个常用的ReentrantLock也是排它锁。
共享锁,一种可以同时被多个线程持有的锁,持有共享锁的线程之间不会相互竞争和阻塞。
排它锁很多时候等同于另外一个名称:写锁,共享锁等同于:读锁。
之所以出现读锁和写锁那是因为系统通常都是读多写少,读锁共享可以大大提高系统性能。
读锁与写锁
不知道大家有没有想过一个问题:既然修改的数据的时候已经加写锁了,写锁排它,那为什么修改完之后,读的时候还要加读锁呢?比如下面这段代码:
private int i = 1;
public synchronized void setI(int i){
this.i = i;
}
public int getI(){
return this.i;
}
有一个线程A调用完setI(2)之后,另外一个线程B调用getI()方法能保证返回值是2吗?答案是:不一定!why?java内存模型。关于java内存模型的简单描述可参考林林:剖析volatile、synchronized实现原理zhuanlan.zhihu.com
那为什么加读锁就可以了呢?因为锁的内存语义是:让线程本地的副本变量无效化,从主内存中获取!。所以上面get()方法加锁读就可以读到最新的值。
所以写锁和读锁总是成对出现,因为只有写锁没有读锁保证不了线程安全。
锁设计
1、可重入
无论是读锁还是写锁,锁一定要设计成可重入的,因为方法本身可以调用自己。如果是锁不重入,线程第一次进来时加锁成功,第二次进来的时候就会阻塞,因为锁被占用了,结果自己阻塞自己,造成死锁。可重入的话,后面再进来,判断加锁线程是自身就不用阻塞了,避免死锁。如何实现可重入?存储加锁的线程的即可。写锁排它,所以任何时候只需要存储一个线程。读锁共享,用ThreadLocal。
2、读锁如何共享、写锁如何排它
参考java中ReentrantReadWriteLock的设计。
用一个32位的int字段,高16位存储是否有读锁,低16位是否有写锁,然后用volatile修饰,保证线程间可见性,更新值时用cas保证原子性。
假设存储字段名称为state,当加读锁时只需要cas(state,state + 1<<16),即可在高16位加1。
加写锁时cas(state, state + 1) 即可在低16位加1。
加锁时如果判断 (state >>> 16) > 0 就表示当前存在读锁,这时可以加读锁,写锁拒绝。
如果是state & ( 1 << 16 -1) > 0 就表示当前存在写锁,除了重入锁的线程外,其它线程的读锁、写锁都不可以加。
释放锁时用cas用还原操作即可。
这样就可以实现读锁的共享和写锁的排它。