LOCK
以Java形式实现的锁
在没有JUC之前,通过synchronized来实现同步,但是我们并不能自己去干预锁的获取和锁的释放,比如锁的释放只能由同步代码块执行完或者异常执行完,通过monitorExit指令来决定。
而使用Lock可以自由的控制锁的释放和锁的获取。
Lock只有唯一的一个实现,ReentrantLock。
ReentrantLock
synchronized和ReentrantLock都是支持可重入的。
当线程发生竞争时其他线程怎么办—-》阻塞。
ReentrantReadWriteLock
重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock
接口。
适合在并发场景下读多写少的场景,也是一种乐观锁,当多线程一起读的时候属于共享锁,只有存在对数据进行修改的时候才会排它使其他线程阻塞,其余情况下均能保证并发性能。
公平锁与非公平锁:
fair: 如果当前同步队列已经有线程在阻塞了,那么就不尝试CAS操作,而是排队
nofair: 不管有没有别的线程阻塞,都尝试CAS获取一下锁。
同步工具
独占—》互斥锁
共享—》读写锁
只有同时读的时候才可以共享,当有一个线程在写的时候读的线程会阻塞,为了实时同步数据。
ReentrantLock依靠AQS来完成线程阻塞的。
锁的基本要素
一个共享数据来标记对象锁的状态 ;
在AQS中使用state来做锁标记,通过CAS原子性操作。
0表示无锁,≥1表示有锁或者重入锁。
final void lock() {
if (compareAndSetState(0, 1)) // 0表示无锁状态,当内存中的偏移量为0时,此时期望值和预期值相等,那么就替换为1.
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- 当第一个线程执行到Lock.lock()时,首先会通过CAS判断内存偏移量是否为0,此时AQS的state属性也为0,那么CAS会返回ture,那么就将当前线程设置为独占访问权的线程。
- 此时假如第二个线程进来,那么AQS判断为false,走acquire()方法;
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 进行一次尝试,如果获取到锁的线程执行完后state会置0
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 此时仍然会进行一次AQS操作判断,这样设计是考虑到前面获取到锁的线程有可能已经执行完毕,此时就不需要再加到AQS同步队列中了
- 如果判断不成立,就会进行自旋,首先创建一个空head,然后将后面进来的线程加入到链表中等待。
- 自旋第一次创建一个空节点,第二次将竞争线程添加到节点中并且prev指向空节点,至少自旋两次,之后就不会进enq方法了。
时序图:
AQS线程阻塞队列
Lock类与synchronized的区别
- 一个是关键字,一个是juc提供的工具类
- 释放锁的区别,一个可以自由控制 lock.unlock(),一个只能等同步代码块执行完或者异常执行,即monitorEnter和monitorExit
- 灵活性
- 死锁的解决方案,可以使用try lock解决
- 公平性和非公平性,使用后者可以自由控制线程竞争的公平性。
synchronized的可重入
直接使monitor监视器+1;
lock的可重入
使AQS类的state属性+1;
可重入锁也是防止死锁的一种方式,当获取到对象锁后,继续在同步代码块中获取这个对象锁,如果是同一个线程,不需要判断是否占有锁而是将对象的monitor监视器+1;
总结
在synchronized中,基于乐观锁和自旋锁优化了synchronized的加锁开销,并且升级成重量级锁之后通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
而在ReentrantLock中则是使用AQS来让竞争失败的线程达到阻塞以及唤醒的。
AQS中维护了一个FIFO的双向链表,竞争失败的线程会被封装成一个node节点,加入到链表的尾部,当占有锁的线程释放锁后唤醒一个线程去获取锁。
Sync有两个具体的实现,一个fair和nofair,即公平锁和非公平锁,公平锁表示所有线程严格按照FIFO链表进行获取锁,非公平锁则是不管当前的FIFO链表上有没有阻塞的线程都会尝试获取一下锁。
Nofair.lock()方法
final void lock() {
if (compareAndSetState(0, 1)) // 即每个线程首次都会尝试CAS操作,如果期望值与预期值相等那么就将state内存偏移量设置为1。
setExclusiveOwnerThread(Thread.currentThread()); //并且将自己的线程ID设置上去
else
acquire(1); // 如果已经有线程占有了锁,上面的条件肯定是不会执行的。 就走acquire方法
}
CAS的实现原理
protected final boolean compareAndSetState(int
expect, int update) {
// See below for intrinsics setup to support
this
return unsafe.compareAndSwapInt(this,
stateOffset, expect, update); // 如果this对象的内存中 expect预期值与state相等,那么就修改为update的值。 成功返回true
}
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,
以及涉及到 state 这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入
锁的实现来说,表示一个同步状态。它有两个含义的表示
1. 当 state=0 时,表示无锁状态
2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为
ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,
比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0
其他线程才有资格获得锁
总的来说CAS修改state属性成功的线程获得独占锁,获取失败的将会自旋生成一个FIFO链表,并且互相指向,使用park()方法阻塞线程挂起,当独占锁线程执行完毕后使用unpark()方法唤醒同步队列中的第二个线程,因为第一个只是为了创建链表使后面的线程可以互相指向的临时变量。
此时第二个线程的状态修改为-1,可以继续通过CAS获取锁。
并且在线程挂起的时候,如果有线程的状态>0 即表示中断状态或者异常,被会移出链表。
AQS所生成的双向链表中Node节点 有 5 中状态,分别是:
CANCELLED(1),SIGNAL(-1)、CONDITION(- 2)、PROPAGATE(-3)、默认状态(0)