线程等待唤醒机制
wait/notify
- Object类中的wait和notify方法实现线程等待和唤醒
- wait和notify方法必须要在同步块或者方法里面,且成对出现使用
先wait后notify才OK ```java public class LockSupportDemo { public static void main(String[] args) throws InterruptedException {
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了");
}, "t1").start();
//暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
}, "t2").start();
}
}
<a name="P7HaU"></a>
### await/signal
1. Condition接口中的await后signal方法实现线程的等待和唤醒
1. Condtion中的线程等待和唤醒方法之前,需要先获取锁
1. 一定要先await后signal,不要反了
```java
public class LockSupportDemo2
{
public static void main (String[]args)
{
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "start");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.lock();
try {
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
}, "t2").start();
}
}
Object和Condition使用的限制条件
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
-
LockSupport
LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。
- LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
- park() /park(Object blocker) :permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。
- unpark(Thread thread) :调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
- 之前错误的先唤醒后等待,LockSupport照样支持
```java
//正常使用+不需要锁块
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " " + "1111111111111");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " " + "2222222222222------end被唤醒");
}, "t1");
t1.start();
//暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " -----LockSupport.unparrk() invoked over");
<a name="QgfPl"></a>
## 线程中断:
参考:[https://www.zhihu.com/question/41048032/answer/89431513](https://www.zhihu.com/question/41048032/answer/89431513)<br />interrupted()是Java提供的一种中断机制,要把中断搞清楚,还是得先系统性了解下什么是中断机制。
<a name="XkYot"></a>
### 什么是中断?
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。
- 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
- 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
- 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
<a name="wMnZj"></a>
### 中断的相关方法
- public void interrupt() <br />将调用者线程的中断状态设为true。
- public boolean isInterrupted() <br />判断调用者线程的中断状态。
- public static boolean interrupted <br />只能通过Thread.interrupted()调用。 <br />它会做两步操作:
1. 返回**当前线程**的中断状态;
1. 将当前线程的中断状态设为false;
<a name="Nk40m"></a>
## AQS
AbstractQueuedSynchronizer 抽象队列同步器。是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态。这里的同步器组件指[CountDownLatch](https://so.csdn.net/so/search?q=CountDownLatch&spm=1001.2101.3001.7020)、Semaphore、ReentrantLock、ReentrantReadWriteLock 等。<br />
<a name="gIQgW"></a>
### **锁和同步器的关系**
- 锁:面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可)
- 同步器:面向锁的实现者(比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。)
<a name="oLtgU"></a>
### 能干嘛
- 加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
- 抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
- 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的 结点(Node),通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
- 
AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个 Node节点 来实现锁的分配,通过**CAS**完成对**State**值的修改。<br />
<a name="UKzDh"></a>
### AQS内部体系架构
<a name="NXY2a"></a>
#### AQS的int变量:
AQS的同步状态State成员变量<br />类比,银行办理业务的受理窗口状态:
- 零就是没人,自由状态可以办理
- 大于等于1,有人占用窗口,等着去
```java
/**
● The synchronization state.
*/
private volatile int state;
AQS的CLH队列
CLH队列(三个大牛的名字组成),为一个双向队列
类比,银行侯客区的等待顾客
小总结:
- 有阻塞就需要排队,实现排队必然需要队列
- state变量+CLH双端Node队列
内部类Node(Node类在AQS类内部)
Node的int变量
Node的等待状态waitState成员变量(注意与status状态区分,status表示同步状态)
类比,等候区其它顾客(其它线程)的等待状态
队列中每个排队的个体就是一个Node.volatile int waitStatus;
Node此类的讲解
内部结构:
属性说明:总结
从我们的ReentrantLock开始解读AQS
ReentrantLock:Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的 ```java package com.example.demo.juc;
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest { public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理 new Thread(() -> { lock.lock(); try { System.out.println(“——-A thread come in”);
try {
TimeUnit.MINUTES.sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "A").start();
//第二个顾客,第二个线程—-》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待, //进入候客区 new Thread(() -> { lock.lock(); try { System.out.println(“——-B thread come in”); } finally { lock.unlock(); } }, “B”).start();
//第三个顾客,第三个线程—-》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待, //进入候客区 new Thread(() -> { lock.lock(); try { System.out.println(“——-C thread come in”); } finally { lock.unlock(); } }, “C”).start(); } }
**初始状态:**<br />
**lock():**<br /><br />**compareAndSetState(0, 1):**<br /><br />该方法会用CAS修改state的值,state默认值是0,所以第一次修改会成功,也就是A线程会修改成功并返回true
**setExclusiveOwnerThread(Thread.currentThread());**<br /><br />该方法当前线程赋值给这个属性,这个属性代表拥有这把锁的线程,所以是A线程拥有这把锁,加锁成功。<br />**这是此时的状态:也就是第一个顾客来之后:**<br /><br />接下来是B线程:<br />因为此时state=1,所以**compareAndSetState(0, 1) **会修改失败,返回false,执行acquire(1);<br /><br />首先执行tryAcquire(arg):这个方法是尝试获取锁,成功返回true,失败返回false<br />里面调用nonfairTryAcquire(acquires);<br />
此时c=getState(),c=1;getExclusiveOwnerThread()=ThreadA;所以该方法返回false,尝试加锁失败
接着会走到addWaiter(Node.**_EXCLUSIVE_**):Node.**_EXCLUSIVE_**初始化为null,tail初始化也是null;所以执行enq(node)方法<br />**注意:这是一个自旋操作,**<br />tail为空节点,会进入if判断,通过CAS操作设置head头结点的指向Node空节点(此时Node节点即图中的傀儡节点,不储存数据,仅用于占位)<br /><br /><br />**此时状态:**<br /><br />然后再将head头结点的执行赋给tail尾结点的指向<br />`tail=head; `<br /><br />完成后,不会走下面的else 分支。由于是自旋,继续从头开始<br />tail不为null,走else分支,
首先:
node.prev = t;//将B线程的前指针指向空节点<br />
然后:<br />compareAndSetTail(t, node) //设置尾结点:将tail尾结点所执向的节点改为执向顾客B<br />
然后:<br />t.next = node; //将空节点的next指针指向顾客B<br /><br />最后:return结束自旋!<br />然后再执行这个方法:acquireQueued(addWaiter(Node.**_EXCLUSIVE_**), arg))<br /><br />释放锁的代码就不看了,非常简单:
```java
LockSupport.unpark(s.thread);
B线程获取到了锁,原来的B节点变成了原来的哨兵节点(因为C跟B的逻辑想差不大,因此没有过多赘述)