Java“锁”记
内置锁和显示锁
内置锁其实是相对显示锁来说的,说白了内置锁就是synchronized
所代表Java原生锁机制,Jdk5.0之后又引入了Lock
及其子类ReentrantLock
这样一种新的锁机制。从加锁和内存语义上二者一样,只不过后者添加了一些其他功能,可以实现诸如轮询锁、超时锁和中断锁的功能。
public interface Lock {
void lock();
void lockInterruptibly() throw InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit)
throw InterruptedException;
void unlock();
Condition newCondition();
}
如果内置锁是一个Lock
的话,它只有lock()
和unlock()
方法。从锁的基本属性上说,内置锁和显示锁都是可重入的,内置锁是非公平的,显示锁还可以设置为公平的。
tryLock
和lock
的区别是前者获得锁返回true,获取不到返回false,都是立马返回,而后者如果获取不到将会阻塞到那里。
另外由于内置锁是自动释放,而显示锁必须手动释放,这就形成了显示锁的调用模式如下面这样:
Lock lock = ...;
lock.lock();
try {
// 逻辑
} finally {
lock.unlock();
}
也就是锁的释放必须放在finally中,确保锁可以释放。
从ReentrantLock
衍生出来一个ReentrantReadWriteLock
,为啥要有读写锁呢?其实是基于这样的原则,读写和写写是会引起线程安全问题的,所以都需要同步,前者是因为可见性,后者是因为一致性,但是读读是不需要同步的,所以讲读写拆分开来以提高性能。这就好比原来大家都排一个队,现在拆成两个队,自然排队等待的时间就短了。
闭锁
闭锁就像一个门,等待一个“事件”开门(结束状态),在开门之前不允许任何人(线程)通过,在此之前大家只能在城门前面等待。只不过城门可以重复的开闭,闭锁只是一次性的。
具体到Java中,闭锁的实现就是CountDownLatch
,它可以用来实现等待某种条件满足后才把线程放行的功能,比如资源就绪、服务启动、某个操作执行等等。
信号量
信号量是用来控制同时访问某个资源的特定数量,或者同时执行某个操作的数量,有点像地铁中的限流。
从某种程度上讲,锁有点像一个二值的信号量,也就是初始值为1的信号量,不同之处是锁是可重入的,信号量不可。
栅栏
栅栏和闭锁类似,它也能阻塞一组线程直到某个事件发生。区别在于栅栏要求线程都到达栅栏位置,才能继续执行,即所谓的闭锁等待的是事件,栅栏等待的是线程。如果对比现实中的例子,闭锁犹如大家去登山,商议好早晨8点出发,无论人齐不齐,到8点大家就出发,而栅栏就类似于大家登一段就在一个歇息点等一等人,等人齐再往上登。
原子变量
原子变量实际上是一种乐观锁技术,即利用冲突检测来判断是否有来自其他线程的干扰,当进行修改操作时,先把变量的当前值current取出来,然后用一个原子的比较交换操作(CAS)对变量进行修改。有两种情况:如果变量的当前值还等于current说明这中间没有线程修改变量,修改变量值为新值;如果当前值不等于current了,说明中间有线程修改变量,重试。
以一个典型的count++为例,大家知道++这种操作实际上包括三步:
- 获取count当前值current
- 当前值加一newvalue
- 将newvalue赋值给count
如果两个线程同时修改count的值,假如两个线程的时序如下:
=====1===========+1=================
=========1===========+1=============
假设count的当前值为1,两个线程分别进行了++的操作,最后的值为2,第一个++操作被“覆盖”了。如果把上面2、3步换成一个CAS操作就不会发生上面的情况了,因为执行第二次操作时会拿count的旧值1和新值2对比,一对比发现不一样,说明其他线程修改了变量,这时候第二个线程会进入下一次的CAS操作,重新获取count值2,比较当前值2等于原来的值,修改为新值3。
原子变量作为一种非阻塞的锁技术,适用在读操作比较多、竞争不那么激烈的场景,这适用于大部分的业务场景。但同时原子变量也有其局限,原子锁只能保证单一变量的线程安全。