锁的分类
自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现使线程再没有获得锁时,不被挂起,而是执行空循环,循环N次后,如果还没获得锁,则被挂起。
阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
重入锁:支持线程再次进入的锁,一般表现为在加锁的方法或加锁的代码块中调用其它加锁的方法或加锁的代码块
读写锁:读锁和写锁竞争,读读共享,读写、写写互斥
互斥锁:每次只能一个线程获得锁,其它线程需要等待
悲观锁:总是认为其它线程会修改共享数据,所以在获取数据时都会加锁,这样其它线程想要获取数据就需要阻塞直达拿到锁为止
乐观锁:总是认为其它线程不会修改共享数据,所以在获取数据时不会加锁,但在更新的时候会判断在此期间,别的线程是否更新过数据,可以使用版本号机制。
公平锁:都通过排队的方式获取锁
非公平锁:刚进来的线程先去获取锁,如果获取到了则使用锁,如果没有获取到,则和其它线程一样去排队获取锁
独占锁:每次只能一个线程能持有锁,类似互斥锁
共享锁:允许多个线程同时获取锁,并发访问共享资源,入读写锁中的读锁
Lock接口介绍
类图关系:
自定义锁(为了后面理解jdk提供的锁)
public class MyLock implements Lock {
//线程是否持有锁,默认为false,表示没有被线程获取
private boolean isHoldLock = false;
//持有锁的线程
private Thread holdLockThread = null;
//重入次数
private int reenTryCount = 0;
/**
* 同一时刻只能有一个线程获得锁,其它线程只能等待
*/
public synchronized void lock() {
//如果锁被持有且持有锁的线程不是当前线程,那么需要等待
if (isHoldLock && Thread.currentThread() != holdLockThread) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果锁没有被持有,则设置锁被持有,当前线程为持有锁的线程,重入次数加1,即为1次
isHoldLock = true;
holdLockThread = Thread.currentThread();
reenTryCount++;
}
@Override
public synchronized void unlock() {
//如果当前线程为锁持有的线程,则重入次数减一,如果重入次数为0,则设置锁为未被线程持有,持有锁的线程为null
if (this.holdLockThread == Thread.currentThread()) {
reenTryCount--;
if (reenTryCount == 0) {
this.holdLockThread = null;
isHoldLock = false;
}
}
}
.................
}
即想要实现重入互斥锁,必须记录锁是否被持有、持有锁的线程、以及重入次数。
AQS原理简述
AQS框架分离了构建同步器时的一系列关注点,它的所有操作都围绕着资源——同步状态(synchronization state)来展开因此,围绕着资源,衍生出三个基本问题:
- 同步状态(synchronization state)的管理
- 阻塞/唤醒线程的操作
- 线程等待队列的管理
同步状态的定义
同步状态,其实就是资源。AQS使用单个int(32位)来保存同步状态,并暴露出getState、setState以及compareAndSetState操作来读取和更新这个状态。
线程的阻塞/唤醒
在JDK1.5之前,除了内置的监视器机制外,没有其它方法可以安全且便捷得阻塞和唤醒当前线程。
JDK1.5以后,java.util.concurrent.locks包提供了LockSupport类来作为线程阻塞和唤醒的工具。
等待队列
等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,因此也叫CLH队列。
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
ReetrantLock源码解析
Lock lock=new ReetrantLock();
lock.lock();
lock.unlock();
构造函数,参数可以为一个Boolean值,也可以为空,为空或者为false,表示创建一个非公平锁。ReentrantLock内部有一个Sync类型的成员变量,几乎所有锁相关的操作,ReentrantLock都是委托sync来完成的。sync继承自AbstractQueuedSynchronizer,也就是说AbstractQueuedSynchronizer类提供了锁相关的操作,但部分方法为抽象方法,需要公平锁和非公平锁去实现。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() { sync.lock(); }
public void unlock() { sync.release(1);}
锁的继承体系入下图所示:
重点属性介绍:
state:表示锁的重入次数,默认为0(类似我们自己实现中的reenTryCount)
exclusiveOwnerThread:表示当前持有锁的线程(类似我们自己实现中的holdLockThread)
我们自己实现中的isHoldLock,在这个体系中,则有继承体系中的unsafe属性调用cas本地方法实现
NonfairSync 非公平锁源码解析
lock源码解析:
public void lock() {
sync.lock();
}
//Sync
final void lock() {
//通过CAS算法调用本地方法设置同步状态,如果设置成功,表示当前线程获取到锁,然后将当前线程设置成锁持有线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//如果没有获取到锁,则去尝试获取(排队获取)
}
//AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试非公平获取锁失败
//addWaiter:在AQS中,维护了一个原生链表(只有头尾节点)来实现队列功能。这个方法把当前请求放到队列尾部
//acquireQueued:让当前线程进入等待状态
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);//尝试非公平获取锁
}
//Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//获取重入次数
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
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//当前节点的上一个节点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire:设置上个节点为等待状态
//parkAndCheckInterrupt:进入等待状态,如果持有锁的线程释放锁后会得到通知,然后返回false,
//如果是被interrupt则返回true,导致最终返回true,抛出异常
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
unlock源码解析:
public void unlock() {
sync.release(1);
}
//AQS
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//计算重入次数
//如果锁持有的线程不是当前线程 则报异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//如果重入次数为0,表示该释放锁了,返回true
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
FairSync 公平锁源码解析
lock源码解析:
//FairSync
final void lock() {
acquire(1);
}
//AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//和非公平锁一样
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors:判断队列中是否有等待且不是自己的线程,有返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//获取到锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//重入逻辑
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
加锁流程:
- 利用cas获取锁,如果获取到锁,则设置当前线程为锁持有线程并返回
- 获取重入次数,如果次数为0,则调用cas抢占锁,如果获取到锁,则设置当前线程为锁持有线程并返回(非公平锁)
- 获取重入次数,如果次数为0,则看队列中是否还有其他等待的线程,如果没有则设置当前线程为锁持有线程并返回(公平锁)
- 如果当前线程为锁持有线程,则增加重入次数,获取到锁并返回
- 将当前线程加入到队列中,并阻塞。
释放锁流程:
- 获取重入次数并减去1,如果新的重入次数为0,则设置持有锁的线程为null,并唤醒其他等待的线程
- 如果新的重入次数不为0,表示该释放操作为重入中的释放
公平锁和非公平锁的加锁过程最大的不同就是非公平锁在lock的时候会去看直接抢占锁,如果抢占不到再排队,公平锁则是看队列是否为空,如果不为空,则去排队。
公平锁和非公平锁的释放锁的过程一样。
如何实现阻塞和唤醒操作的呢?
用synchronized时,阻塞等待用的是wait,唤醒等待用的是notify
在CAS乐观锁体系中,包括ReentrantLock、ReentrantReadWriteLock等,阻塞是用的UNSAFE.park(false, 0L);,唤醒是用的UNSAFE.unpark(thread);
具体源码如下:
//阻塞源码
//AQS doAcquireShared
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞当前线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);//调用本地方法实现阻塞操作
setBlocker(t, null);
}
//唤醒阻塞线程
//AQS释放锁时
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒队列中的第一个被阻塞的线程
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒阻塞的线程
LockSupport.unpark(s.thread);
}
//LockSupport
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);//调用本地方法实现阻塞线程的唤醒
}