一、Lock接口
Lock实现提供了比使用Synchronized方法和语句更广泛的搜定操作,此操作允许更灵活的结构,可以具有很大的属性,可以支持多个相关的Condition对象。
public interface Lock {
//获取锁
void lock();
//如果当前线程未被中断,则获取其锁
void lockInterruptibly() throws InterruptedException;
//尽在调用时锁为空闲状态时才获取该锁
boolean tryLock();
//如果所在给定的等待时间内空闲,并且当前线程未被中断,则获取其该锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//返回绑定到此的Lock,实例新的Condition 实例
Condition newCondition();
}
与Synchronized的区别:
- Lock实现类的锁必须是显性的调用加锁、释放锁,且必须成对出现
- 加锁操作可以是方法内部的核心代码片段.加锁力度小,意味着并发性更高
- 可以通过lockInterruptibly方式添加可中断锁
- 可以尝试性加锁,未抢到锁可以立即返回
二、ReentrantLock
1.公平性锁与非公平性锁
如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被"饥饿致死"正是因为它得不到CPU运行时间的机会。解决饥饿的方案被称之为"公平性".即所有线程均能公平地获得运行机会。通俗讲,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。
线程饥饿的原因:
- 高优先级的线程会比低优先级的线程优先执行
- 线程被阻塞在一个等待进入同步块的状态
ReentrantLock reentrantLock = new ReentrantLock(true);
//底层代码:
public ReentrantLock(boolean fair) {
this.sync = (ReentrantLock.Sync)(fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
//传入的Boolean值为true则表示公平性锁 若为false则表示非公平性锁
******************************************
ReentrantLock reentrantLock = new ReentrantLock();
//未传参数的底层代码
public ReentrantLock(){
this.sync = new ReentrantLock.NonfairSync();
} //其默认的是false
2.公平性锁
在ReentrantLock中通过FairSync实现公平性锁,其实现是基于一个队列来实现的
public class demo implements Runnable{
private static Integer num=0;
private ReentrantLock rtl;
public demo(ReentrantLock reentrantLock) {
this.rtl=reentrantLock;
}
@Override
public void run() {
while(true){
//加锁
rtl.lock();
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
//释放锁
rtl.unlock();
}
}
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(true);
new Thread(new demo(reentrantLock),"A").start();
new Thread(new demo(reentrantLock),"B").start();
}
}
结果截取:A、B线程交替执行
3.非公平性锁
在锁中保持一个获取锁的线程信息,当释放锁之后再次抢锁时,通过比较正在抢锁的线程和队列头的线程,如果那个是上一次获取锁的线程,那么那个线程具有优先执行权。
简单来说就是,第一次抢到锁的线程,再再次进行抢锁的过程中是具有优先权的。
public class demo implements Runnable{
private static Integer num=0;
private ReentrantLock rtl;
public demo(ReentrantLock reentrantLock) {
this.rtl=reentrantLock;
}
@Override
public void run() {
while(true){
//加锁
rtl.lock();
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
//释放锁
rtl.unlock();
}
}
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(false);
new Thread(new demo(reentrantLock),"A").start();
new Thread(new demo(reentrantLock),"B").start();
}
}
结果截取:如节选结果所示,B线程执行一段时间后换A线程执行
4.ReentrantLock 方法实现
公平性锁:
锁空闲时:
1.当前队列中没有节点。当前线程就可以抢锁成功
2.队列中有数据, 且当前线程等于队列中第一个线程,故可以抢锁成功。
锁被占用时:
队列中的第一个节点的线程与上一次占用锁的线程相同时,可以抢锁成功。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
公平锁的tryAcquire,除非是递归调用没有等待或者是第一个,否则不许访问
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 不存在已经等待很久的线程并且比较并且设置状态成功
setExclusiveOwnerThread(current);//加锁成功
return true;
}
}
//所部空闲的时候,比较当前线程是否和上一次获取锁的线程相等
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//加到最高符号位的时候nextc<0
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//更新AQS中的state的值
setState(nextc);
return true;
}
return false;
}
}
********************************
//上述代码中的hasQueuedPredecessors实现
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
/** 双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,
是在第二个节点开始的。当h != t时: 如果(s = h.next) == null,等待队列正在有线程进行初始化,
但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True
如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread ==
Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可
以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当
前线程不同,当前线程必须加入进等待队列。*/
非公平性加锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//CAS直接修改锁的状态
if (compareAndSetState(0, 1))
//当前线程获得锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
******************************************
//
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//此时判断c==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;
}
重入锁实现:
- ReentrantLock都是把具体实现委托给内部类(Sync、NonfairSync、FairSync)
- ReentrantLock的重入计数是使用AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用、等于0表示空闲,小于O则是重入次数太多导致溢出了.
- 可重入锁需要一个重入计数变量,初始值设为0,当成功请求锁时加1,释放锁时减1,当释放锁之后计数为0则真正释放锁;
- 重入锁还必须持有对锁持有者的引用,用以判断是否可以重入;
condition
synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock同样可以实现该功能,但是要借助于Condition对象。
newCondition
public Condition newCondition()
返回用来与此Lock实例一起使用Condition实例。
public interface Condition {
//使当前线程进入休眠进行等待
void await( ) throws InterruptedException;
void awaitUninterruptibly() ;
long awaitNanos ( long nanosTimeout) throws InterruptedException;
boolean await(long time,TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒因await进入体眠的一个线程
void signal() ;
//唤醒因await进入休眠的所有线程
void signalAll();
}
注意:
使用await、signal、signalAll是必须加锁的,使用重入锁的加锁释放锁
await、signal要通知的线程必须是作用于同一个Condition实例
Condition与Object中的wati,notify,notifyAll区别;
1.Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll)方法。
不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
2.Condition它更强大的地方在于∶能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区︰当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,“写线程"需要等待;当缓冲区为空时,“读线程"需要等待。
如果采用Object类中的wait().,notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。但是,通过Condition,就能明确的指定唤醒读线程。
三、AQS
在深入介绍锁之前,需要先介绍一下AQS (AbstractQueuedSynchronizer)是J.U.C最复杂的一个类,我们先简单研究一下。
AQS里面有三个核心字段:
private volatile int state;
private transient volatile Node head;private transient volatille Node tail;
其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。
AQS中的state :
state=0表示锁是空闲状态
state>0表示锁被占用
state<0表示溢出
static final class Node {
/** 标记表示结点正在共享线程中等待 */
static final Node SHARED = new Node();
/** 标记表示节点正在独占模式下等待 */
static final Node EXCLUSIVE = null;
/** 表示线程已经被取消
同步队列中的线程因为超时或中断,需要从同步队列中取消。被取消的节点将不会有任何改变
*/
static final int CANCELLED = 1;
/**后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
*/
static final int SIGNAL = -1;
/** 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会中等待队列中转移到同步队列中,加入到对同步状态的获取
*/
static final int CONDITION = -2;
/**
* 下一次共享模式同步状态获取将会无条件的被传播下去
*/
static final int PROPAGATE = -3;
/**
等特状态,仅接受如下状态中的一个值:
SIGNAL:-1
CANCELLED:1
CONDITION:-2
PROPAGATE:-3
0:初始化的值
对于正常的同步节点,它的初始化值为0,对于条件节点它的初始化的值是CONDITION。它使用CAS进行修改。
*/
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//获取其同步状态的线程
volatile Thread thread;
/**
筰待队列中的后继节点。如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说节点类型(独占和共享)和等特队列中的后继节点公用同一个字段
*/
Node nextWaiter;
//如果节点在共享模式下等待则返回true
final boolean isShared() {
return nextWaiter == SHARED;
}
//获取前驱节点
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
volatile int waitStatus:节点的等待状态,一个节点可能位于以下几种状态:
- CANCELLED = 1
当前的线程被取消,节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。 - SIGNAL= -1
表示当前节点的后继节点包含的线程需要运行,也就是unpark节点的继任节点是〔或者将要成为)BLOCKED状态(例如通过LockSupport .park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。 - CONDITION = -2
当前节点在等待condition,也就是在condition队列中.表明节点对应的线程因为不满足一个条件(Condition)而被阻塞 - PROPAGATE=-3当前场景下后续的acquireShared能够得以执行
当前节点在sync队列中,等待着获取锁
正常状态,新生的非CONDITION节点都是此状态。非负值标识节点不需要被通知(唤醒)。
volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。
volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。
volatile Thread tffread;节点绑定的线程。
Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。
节点(Node)是构成CHL的基础,同步器拥有首节点(head)和尾节点(tail) ,没有成功获取同步状态的线程会构建成一个节点并加入到同步器的尾部。CHL的基本结构如下:
AQS同步原理
获取锁︰首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。
释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。
要支持上面两个操作就必须有下面的条件:
- 原子性操作同步器的状态位
- 阻塞和唤醒线程
- 一个有序的队列