synchronized 重量级锁分析
1. 背景
在JDK1.6以前,synchronized 的工作方式都是这种重量级的锁。它的实现原理就是利用 kernel 中的互斥量,mutex。主要是内核中的mutex 能够保证它是一个互斥的量。如果线程1拿到了 mutex,那么线程2就拿不到了。这是内核帮我们保证的。
至于为什么可以,可以去了解一下内核中的互斥量。
2. 为啥叫做重量级锁
内核需要去申请这个互斥量,必须要进入内核态。也就是这里需要用户态,内核态的切换。状态的切换,开销是比较大的。这就是重型锁的一个弊端。对于重型锁的一些主要的封装都是在 c 语言中的 pthread 这样一个库中,比如mutex_init, mutex_lock 等。之所以叫重量级锁,就是因为需要进入到内核态。
3. 能否在用户态实现一把互斥锁
我们不需要进入到内核态就能够获取到这样的一把锁,也就是在用户态就可以实现一把锁。
比如说,现在就有一把锁,就叫做 state。0:表示未被使用,1 表示锁被占用了。
lock 的实现
如果锁当前是被占用的状态,那么程序就一直死循环。如果某个时刻,有一个线程把锁释放了,那么就退出死循环,执行下一行 state = 1。而且持有者=当前线程。也就是 state = 0的时候,就可以任务当前线程可以拿到这把锁了。
unlock 的实现
释放锁的时候,首先需要判断一下当前持有锁的线程是不是当前线程。如果是当前线程,那么就将 state 置为0, 持有者 = null。
当前设计方式存在的问题
while(state==1); state = 1;其实可以认为这个是比较赋值俩步操作,先比较,符合条件了,再进行赋值。那么其实这不是原子性的。比如说在比较的时候,俩个线程同时进行了比较,这俩个线程同时发现state = 0,那么这俩个线程同时都去会执行 state = 1的操作,这俩个线程都认为自己拿到了锁,那么这个就产生了并发的问题了。
如何改进这个问题呢?
我们看到比较和赋值不是原子性的,在软件层面,我们也无法保证这俩步的原子性。所以,计算机给我们提供了原语,也就是 CAS。
那么我们来看一下如何改进呢? 我们把比较赋值这俩步操作,变成了一个原语操作,CAS。比较并交换。CAS中有三个参数,cas(state,0,1)。比较state的当前值是不是0,如果是0,那么就赋值为1。
再看看一下 unlock的操作。 在unlock操作中,其实也是一个比较赋值的操作,首先判断当前持有者是不是当前线程,如果是,再进行赋值。那为什么这里不需要用cas呢?因为必定是持有锁的线程才能执行到下一行。
而且赋值操作中,先赋值 持有者 = null。再赋值 state = 0。因为如果先执行 state = 0, 那么就相当于先释放掉这把锁了,另外一个线程就会执行 lock 成功,拿到锁,持有者 = 当前线程。那么就可能会带来一些问题。因为此时释放掉的锁并不是当前线程持有的锁。
这种锁也叫做自旋锁。
自旋锁的优点与缺点
自旋锁,spinLock。自旋锁不需要进入到内核态,整个程序的执行都是在用户态的,包括cas。但是cas不是计算机的原语吗?因为计算机的指令和用户态,内核态没有关系。
自旋锁当然也有缺点。 lock 操作如果一直没有拿到锁的话,会一直尝试。这是需要消耗cpu资源的。 所以自旋锁对于锁竞争比较激烈的情况下,是不适用的。
总结
mutex锁和自旋锁各有优缺点,那么我们能不能把这俩者结合一下呢? JDK 1.4 以前是借助内核中的 Mutex 互斥量; JDK1.4 以后是利用自旋锁,自旋n次以后,还是没有拿到锁的话,就切换到mutex。也就是将自旋锁和mutex进行了一个结合。因为mutex 加锁失败以后,会挂起,让出cpu资源。这样的话,算是对资源的一个合理利用。 JDK1.4以前,我们是可以设置自旋次数的,但是1.6以后,JDK可以自适应自旋,不用设置这个参数了。
当然现在我们所说的都是重量级锁。包括mutext, 自旋锁,自适应自旋锁。