前言
这篇文章主要是以Object类的wait, notify, notifyAll 为主体方法,描述等待通知范式是什么?如何实现的?还有就是通过等待超时的方法实现一个线程池。
1.等待通知的标准范式
要了解该范式,我们得先从3个方法说起。
- wait() : 对象调用wait方法之前需要先获取锁。调用方法之后,该锁的标志位将会去除,并进入对象的等待池中。(只有等待notify或notifyAll 方法唤醒后才能重新获取锁)。该方法还有相关的重载方法 wait(long timeout), 这个表示当我们休眠了timeout的时间会被重新唤醒,不过在休眠时间内仍然可以被notify 或notifyAll方法唤醒。
- notify() 或notifyAll(): 使用该方法之前必须先获取锁,该方法调用之后,本身不会去释放锁,而是伴随着同步代码块的释放而释放(所以该方法建议放在同步代码块的最后)。
- notify() 和 notifyAll() 的异同:两个方法都能唤醒在该对象等待的线程,不过notify() 只能随机唤醒一个,而notufyAll() 能唤醒全部等待的线程,在使用的过程中,一般建议使用notifyAll(), 因为使用notify() 容易造成信号丢失(下面会通过一个实例说明)。
等待通知的标准范式
等待方:
1.获取对象的锁
2.循环判断条件是否满足,不满足则调用wait方法
3.条件满足则执行业务逻辑
通知方:
1.获取对象的锁
2.改变条件
3.通知所有等待在对象的线程
2. 等待通知范式的实例
业务需求:我们现在有一个快递类,它有里程数和地点这两个变量。有两个方法来表示里程数和地点的等待方;还有另外两个方法来表示这两个变量的通知方。(方法上刚好满足上面的范式)
Express.class
package 等待和通知范式;
public class Express {
public final static String CITY = "ShangHai";
private int km; //快递运输的里程数
private String site; //快递到达的地点
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
//通知方
//变化公里数,然后通知处于wait状态需要处理公里数线程进行业务逻辑
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
//通知方
//变化地点,然后通知处于wait状态需要处理地点线程进行业务逻辑
public synchronized void changeSite(){
this.site = "BeiJin";
notifyAll();
}
//等待方
public synchronized void waitKm(){
while(this.km <= 100){
try {
wait();
System.out.println("check km thread["+
Thread.currentThread().getId()+"]"+" is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is "+this.km+", I will change db.");
}
//等待方
public synchronized void waitSite(){
while(this.site.equals(CITY)){
try {
wait();
System.out.println("check site thread["+
Thread.currentThread().getId()+"]"+" is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is "+this.site+", I will call user.");
}
}
测试类:让3个等待里程数变化和3个等待地点变化的线程运行,并在主方法中调用相关的通知方法,观察控制台的输出。
TestWN.class
package 等待和通知范式;
// 测试 wait/notify/notifyAll
public class TestWN {
private static Express express = new Express(0, Express.CITY);
//检查里程数据变化的线程,不满足一直等待
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
//检查地点变化的线程,不满足条件,线程一直等待
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) { //3个地点线程
new CheckSite().start();
}
for (int i = 0; i < 3; i++) { //3个里程数的线程
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();
}
}
这时是调用了notifyAll() 的方法,6个线程都会被唤醒,但是3个地点变化的线程由于没有满足while的判断,会一直处于阻塞状态。
当我们把上面的notiofyAll() 改为 notify() 时。(即在通知方法上更改)
这时我们可以看到它会随机唤醒一个线程,此时它刚好唤醒的是地点(而我们改变的确是公里数),这时候,地点的线程仍然会处于阻塞状态,而公里变化的线程却没有得到通知,这就是我们所说的信号丢失(所以我们一般建议使用notifyAll)
3. 等待超时实现一个连接池
我们平时操作数据库时,由于连接的资源十分珍贵,我们经常使用一个连接池来存取数据库的连接。下面使用一种等待超时的方式实现。
相关的伪代码:(类似于等待通知的范式,不过我们此时可以不用一直等待,当remain时间走完还拿不到相应的连接资源时,我们就可以跳出该循环。)
假设等待时长为 T , 当前时间 now + T 以后超时
long overtime = now + T
long remain = T; //等待的持续时间
while(result 不满足条件 && remain > 0){
wait(remain);
remain = overtime - now; //等待剩下的持续时间
}
其他逻辑代码
<1> 编写一个实现了数据连接的实例
(这里我只重写了createStatement和commit 方法,还有加上一个拿数据库连接的方法,其余原方法照旧)
public class SqlConnectImpl implements Connection {
//拿到一个数据库连接
public static final Connection fetchConnection(){
return new SqlConnectImpl();
}
//模拟生成操作数据库对象的时间
@Override
public Statement createStatement() throws SQLException {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
//模拟提交数据库的时间
@Override
public void commit() throws SQLException {
try {
Thread.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
.................其余重写方法省略(都是原方法).................
<2> 实现一个数据库连接池
用一个LinkedList来存放连接资源。
初始化数据库连接池
//数据库池的容器
private static LinkedList<Connection> pool = new LinkedList<>();
//初始化数据库连接池
public DBPool(int initialSize) {
if(initialSize > 0){
for (int i = 0; i < initialSize; i++) {
pool.add(SqlConnectImpl.fetchConnection());
}
}
}
使用等待超时模式实现拿数据库连接
[1] 当我们等待时间小于0时,表示获取资源时,可以无限等待,直到获取资源后返回
[2] 当我们等待的时间大于等于0时,线程池会等待相应的时间,当池子不为空(表示我们有机会获取连接)或等待已超时(表示我们获取时间过长已经无须等待了)都可以跳出循环。
[3] 跳出循环后,尝试获取连接。
//使用等待超时模式实现拿数据库连接
public Connection fetchConn(long mills) throws InterruptedException {
synchronized (pool){
//若等待时间小于0,表示可用无限等待
if(mills < 0){
while(pool.isEmpty()){
pool.wait();
}
return pool.removeFirst();
}else{
long overTime = mills + System.currentTimeMillis();
long remain = mills;
while(pool.isEmpty() && remain > 0){
pool.wait(remain);
remain = overTime - System.currentTimeMillis();
}
//跳出循环时,可能是超时也可能是连接池为空
Connection result = null;
if(!pool.isEmpty()){
result = pool.removeFirst();
}
return result;
}
}
}
返回数据库连接
[1] 在数据库连接池中放入相应的连接
[2] 通知其他等待的线程
//返回数据库连接
public void releaseConn (Connection conn){
if(conn != null){
synchronized (pool){
pool.addLast(conn);
//通知等待拿数据库连接的线程
pool.notifyAll();
}
}
}
数据库连接池的完整代码
package 数据库连接池;
import java.sql.Connection;
import java.util.LinkedList;
/**
* @Auther: Gs
* @Date: 2020/8/10
* @Description: 数据库连接池
* @version: 1.0
*/
//实现一个数据库连接池
public class DBPool {
//数据库池的容器
private static LinkedList<Connection> pool = new LinkedList<>();
//初始化数据库连接池
public DBPool(int initialSize) {
if(initialSize > 0){
for (int i = 0; i < initialSize; i++) {
pool.add(SqlConnectImpl.fetchConnection());
}
}
}
//使用等待超时模式实现拿数据库连接
public Connection fetchConn(long mills) throws InterruptedException {
synchronized (pool){
//若等待时间小于0,表示可用无限等待
if(mills < 0){
while(pool.isEmpty()){
pool.wait();
}
return pool.removeFirst();
}else{
long overTime = mills + System.currentTimeMillis();
long remain = mills;
while(pool.isEmpty() && remain > 0){
pool.wait(remain);
remain = overTime - System.currentTimeMillis();
}
//跳出循环时,可能是超时也可能是连接池为空
Connection result = null;
if(!pool.isEmpty()){
result = pool.removeFirst();
}
return result;
}
}
}
//返回数据库连接
public void releaseConn (Connection conn){
if(conn != null){
synchronized (pool){
pool.addLast(conn);
//通知等待拿数据库连接的线程
pool.notifyAll();
}
}
}
}
<3> 测试类
我们初始化一个具有10个连接的线程池,此时我们开启50个线程,每个线程分别操作20次数据库。当在规定时间没有拿到连接时,我们视为连接超时的线程。其余获取成功的线程,可以进行相应的操作,最后统计这1000次操作中,有多少次成功,多少次失败。
相关的工作线程
拿到连接的线程,让Got(原子类)自增,并模拟相关的操作后释放连接;在规定时间没有获取连接失败的让notGot (原子类)自增;而end是我们的一个计数器,表示每运行完一个线程就-1。
//工作线程
static class Worker implements Runnable{
int count;
AtomicInteger got; //表示拿到连接的数量
AtomicInteger notGot ; //表示没有拿到连接的数量
public Worker(int count, AtomicInteger got, AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
@Override
public void run() {
while(count > 0){
try{
//从线程池中获取连接,如果1000ms内无法获取,将会返回null
//分别统计获取的数量got和未获取的数量notGot
Connection connection = pool.fetchConn(1000);
if(connection != null){
try{
connection.createStatement();
connection.commit();
}finally {
pool.releaseConn(connection);
//先自增再拿
got.incrementAndGet();
}
}else{
notGot.incrementAndGet();
System.out.println(Thread.currentThread().getName()
+"等待超时");
}
}catch (Exception e){
}finally {
count--;
}
}
//一个工作线程走完就 -1
end.countDown();
}
}
初始化具有10个连接资源的线程池,启动50个线程,每个线程操作20次;由于运行时需要一定的时间,我们使用CountDownLatch 让主线程在工作线程运行时处于阻塞状态,直到全部运行完,才放行并统计最终结果。
static DBPool pool = new DBPool(10);
//控制器: 控制main线程将会等待所有的worker线程结束后才能继续执行
static CountDownLatch end;
public static void main(String[] args) throws InterruptedException {
//线程数量
int threadCount = 50;
end = new CountDownLatch(threadCount);
int count = 20; //每个线程操作的次数
AtomicInteger got = new AtomicInteger(); //计数器: 统计可以拿到连接的线程
AtomicInteger notGot = new AtomicInteger(); //计数器: 统计没有拿到连接的线程
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new Worker(count, got, notGot),
"work_"+i);
thread.start();
}
end.await(); //main线程在此处等待
System.out.println("总共尝试了:"+(threadCount * count));
System.out.println("查到连接的次数:"+got);
System.out.println("没能连接的次数:"+notGot);
}
测试类的完整代码
package 数据库连接池;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class DBPoolTest {
static DBPool pool = new DBPool(10);
//控制器: 控制main线程将会等待所有的worker线程结束后才能继续执行
static CountDownLatch end;
public static void main(String[] args) throws InterruptedException {
//线程数量
int threadCount = 50;
end = new CountDownLatch(threadCount);
int count = 20; //每个线程操作的次数
AtomicInteger got = new AtomicInteger(); //计数器: 统计可以拿到连接的线程
AtomicInteger notGot = new AtomicInteger(); //计数器: 统计没有拿到连接的线程
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new Worker(count, got, notGot),
"work_"+i);
thread.start();
}
end.await(); //main线程在此处等待
System.out.println("总共尝试了:"+(threadCount * count));
System.out.println("查到连接的次数:"+got);
System.out.println("没能连接的次数:"+notGot);
}
//工作线程
static class Worker implements Runnable{
int count;
AtomicInteger got;
AtomicInteger notGot;
public Worker(int count, AtomicInteger got, AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
@Override
public void run() {
while(count > 0){
try{
//从线程池中获取连接,如果1000ms内无法获取,将会返回null
//分别统计获取的数量got和未获取的数量notGot
Connection connection = pool.fetchConn(1000);
if(connection != null){
try{
connection.createStatement();
connection.commit();
}finally {
pool.releaseConn(connection);
//先自增再拿
got.incrementAndGet();
}
}else{
notGot.incrementAndGet();
System.out.println(Thread.currentThread().getName()
+"等待超时");
}
}catch (Exception e){
}finally {
count--;
}
}
//一个工作线程走完就 -1
end.countDown();
}
}
}
控制台输出(它可以统计获取成功和获取失败的连接数,表明我们实现的数据库连接类是正确的。)