AQS
LockSupport、Synchronized、Condition
java中3种让线程等待唤醒的方法:
-
使用Object的wait()让线程等待,使用Object中的notify方法唤醒线程;
-
使用JUC中Condition的await()方法让线程等待,使用Signal()方法唤醒线程;
-
LockSupport可以阻塞当前线程以及唤醒指定被阻塞的线程;
-
Object中的wait()和notify()实现线程的等待唤醒;
1)wait和notify方法必须要在同步块或者同步方法里面成对出现;
wait和notify方法两个都去掉同步代码块后看运行效果出现异常情况:Exception in thread “A” Exception in thread “B”java.lang.IllegalMonitorStateException
2)先wait后notify才可以(如果先notify后wait会出现另一个线程一直处于等待状态)
3)synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法) -
Condition接口中的await和signal方法实现线程等待和唤醒;
LockSupport
- 是一个线程阻塞工具类;可以让线程在任意位置阻塞,阻塞之后也有唤醒方法;底层调用Unsafe的native方法
- 通过park()与unpark()来实现阻塞和唤醒线程操作;
- 官网:LockSupport类使用一种名为Permit的概念来做到阻塞和唤醒线程功能,每个线程有一个permit,permit只有两个值1和0,默认为0;
可以把Permit许可看成一种(0,1)的信号量;不同的是许可的累加上限为1;
阻塞方法park
底层是unsafe类native方法
public static void park() {
UNSAFE.park(false, 0L);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
唤醒方法unpark
- 调用unpark(thread)方法后,就会将thread线程的许可permit设置为1(注意多次调用unpark方法,不会累加,permit值还是1),会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法立刻返回;
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport解决的问题
1)不用持有锁块,不用加锁,程序性能好;
2)先后顺序,不容易卡死;(因为unpark获得了一个凭证,之后再调用park方法,既可以消费,不会阻塞)
/*
(1).阻塞
(permit默认是O,所以一开始调用park()方法,
当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,
park方法会被唤醒,然后会将permit再次设置为O并返回)
static void park()
static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
(调用unpark(thread)方法后,就会将thread线程的许可permit设置成1
(注意多次调用unpark方法,不会累加,
permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
static void unpark(Thread thread)
* */
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
LockSupport.park();
/*
如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
所以下一行被注释的打开会导致程序处于一直等待的状态
* */
//LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
},"A");
t1.start();
//下面代码注释是为了A线程先执行
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
Thread t2=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
//有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
LockSupport.unpark(t1);
//LockSupport.unpark(t1);
},"B");
t2.start();
}
}
- 每个线程最多只能对一个线程最多发放一次许可证permit,但是每个线程可以对多个线程发放一次许可证permit
- 一个线程中如果有两个或者多个LockSupport的park方法:则需要多个线程进行发放许可证permit
AQS
- 是用来构建锁或者其他同步器组件的重量级基础框架
- 通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status的值的修改(0表示没有,1表示阻塞)
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现,他将请求共享资源的线程封装成一个个Node装进队列,通过CAS,自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步效果。
AQS具备特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平锁
- 可重入
- 允许中断
除了ReentrantLock外,JUC下其他的同步器组件都是基于AQS框架实现的;
- 一般通过定义内部类Sync继承AQS
AQS数据结构
- state
state表示资源的可用状态,如果大于0,则说明锁已经被占用了。如果等于0,则说明锁没有被占用。 - head
指向队列的头结点; - tail
指向队列的尾结点; - Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
- AQS内部定义了两个队列
- 同步等待队列
- 条件等待队列
同步等待队列
AQS同步等待队列也称CLH队列。是一种双向链表数据结构的队列,按照FIFO先入先出获取锁;
条件等待队列
Condition是一个多线程协调通信的工具类,某些线程等待某个条件,当条件具备时,等待线程被唤醒,从而争夺锁;
AQS数据结构源码
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//结点的状态
volatile int waitStatus;
//指向上一个结点
volatile Node prev;
//指向下一个结点
volatile Node next;
//将获取锁失败的线程封装为一个个的Node结点,这里封装了获取锁失败的线程;
volatile Thread thread;
Node nextWaiter;
...
}
ReentrantLock开始解读AQS
在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)。
ReentrantLock的构造函数:不传参数默认创建一个非公平锁,参数为true为公平锁,参数为false为非公平锁。
代码如下:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
以NonfairSync为例:
在 ReentrantLock 中,NoFairSync 和 FairSync 中 tryAcquire() 方法的区别,可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
hasQueuedPredecessors() 方法是公平锁加锁时判断等待队列中是否存在有效节点的方法
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
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());
//1.Node s= h.next;头结点的下一个结点
//2. s==null 判断头结点的下一个结点是否为空节点
//头节点指向的是哨兵结点,判断其下一个结点是否为空,为空则队列为空
//3. || s.thread!=Thread.currentThread()
//判断哨兵结点的下一个结点封装的线程是否和当前线程相等。
}
篇幅太长、进入第二篇